@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.
- package/CHANGELOG.md +38 -0
- package/app.config.ts +10 -10
- package/components/Attention.vue +269 -0
- package/components/Auth/LoginForm.vue +1 -1
- 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/MobileBlocker.vue +5 -5
- package/components/MobileLoginPicker.vue +13 -13
- 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/Organization/Picker.vue +3 -3
- 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/useAuth.js +1 -1
- package/composables/useClipboard.js +79 -0
- package/composables/useConfetti.js +194 -0
- package/composables/useContext.js +53 -5
- package/composables/useFile.js +189 -0
- package/composables/useMobileGuard.ts +21 -21
- package/composables/useModal.js +71 -0
- package/composables/useOrganization.ts +3 -3
- 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/docs/superpowers/plans/2026-05-27-nuxt-apps-to-contexts-rename.md +630 -0
- package/docs/superpowers/specs/2026-05-27-nuxt-apps-to-contexts-rename-design.md +113 -0
- package/middleware/{03.apps.global.ts → 03.contexts.global.ts} +25 -25
- package/nuxt.config.ts +1 -1
- package/package.json +13 -8
- package/stores/modal.js +48 -0
- package/stores/toast.js +156 -117
- 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 {
|
|
3
|
+
import type { ContextDefinition } from '~/configs/apps'
|
|
4
4
|
|
|
5
5
|
const props = defineProps<{
|
|
6
|
-
/**
|
|
7
|
-
blockedApp?:
|
|
8
|
-
/**
|
|
9
|
-
fallbackApp?:
|
|
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 {
|
|
4
|
+
import type { ContextDefinition } from '~/configs/apps'
|
|
5
5
|
|
|
6
6
|
const props = defineProps<{
|
|
7
|
-
/**
|
|
8
|
-
options:
|
|
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
|
|
15
|
-
pick: [
|
|
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="
|
|
50
|
-
:key="
|
|
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',
|
|
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(
|
|
58
|
-
:is="resolveIcon(
|
|
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
|
-
{{
|
|
64
|
+
{{ context.label }}
|
|
65
65
|
</div>
|
|
66
|
-
<div v-if="
|
|
67
|
-
{{
|
|
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
|
-
<
|
|
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>
|