@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.
- package/components/Attention.vue +269 -0
- package/components/Chart/index.vue +193 -0
- package/components/Container.vue +116 -0
- package/components/CopyButton.vue +98 -0
- package/components/Files/Card.vue +215 -0
- package/components/Files/Preview.vue +675 -0
- package/components/Files/Upload.vue +408 -0
- package/components/Files/UploadModal.vue +83 -0
- package/components/Forms/InputPin.vue +162 -0
- package/components/InfoToolbar.vue +54 -0
- package/components/Kanban/Standard.vue +588 -0
- package/components/Loader.vue +358 -0
- package/components/Modal/Base.vue +6 -0
- package/components/Modal/Confirm.vue +89 -0
- package/components/Modal/DeleteConfirm.vue +14 -31
- package/components/Modal/Form.vue +76 -0
- package/components/Modal/Host.vue +58 -0
- package/components/Modal/Prompt.vue +74 -0
- package/components/Modal.vue +119 -34
- package/components/Table/Standard.vue +198 -25
- package/components/Table/index.vue +169 -369
- package/components/Table/views/ViewCards.vue +74 -0
- package/components/Table/views/ViewDatabase.vue +236 -0
- package/components/Table/views/ViewTable.vue +299 -0
- package/components/Toast/Alert.vue +16 -111
- package/components/Toast/Container.vue +4 -29
- package/components/Toast/Notification.vue +8 -43
- package/components/Toast/Process.vue +8 -86
- package/components/Toast/Stack.vue +59 -0
- package/components/Toast/Standard.vue +115 -0
- package/components/Tree/Standard.vue +49 -4
- package/components/Workflow/Board.vue +356 -0
- package/components/Workflow/Builder.vue +454 -0
- package/components/Workflow/Form.vue +526 -0
- package/components/Workflow/Viewer.vue +225 -0
- package/components/Workflow/edges/TransitionEdge.vue +84 -0
- package/components/Workflow/nodes/StepNode.vue +88 -0
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useFile.js +189 -0
- package/composables/useModal.js +71 -0
- package/composables/useToast.js +38 -65
- package/composables/useWorkflow/form.js +162 -0
- package/composables/useWorkflow/index.js +18 -0
- package/composables/useWorkflow/view.js +220 -0
- package/composables/useWorkflowLayout.js +162 -0
- package/css/tokens.css +10 -10
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- 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
|
-
<
|
|
19
|
+
<ModalConfirm
|
|
22
20
|
:model-value="modelValue"
|
|
21
|
+
severity="danger"
|
|
23
22
|
:title="title"
|
|
24
|
-
|
|
25
|
-
:
|
|
26
|
-
:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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>
|