@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,269 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* <Attention> — Wrapper que aplica efectos de "llamar la atención" a su slot.
|
|
4
|
+
*
|
|
5
|
+
* Efectos:
|
|
6
|
+
* - 'ping' → ondas de radar concéntricas saliendo del elemento
|
|
7
|
+
* - 'pulse' → respiración (scale 1 → 1.05)
|
|
8
|
+
* - 'shake' → vibración tipo campana (rotate)
|
|
9
|
+
* - 'bounce' → rebote vertical
|
|
10
|
+
* - 'glow' → halo de color difuminado
|
|
11
|
+
* - 'shimmer-border' → borde con gradiente cónico multicolor girando
|
|
12
|
+
*
|
|
13
|
+
* Props clave:
|
|
14
|
+
* - active → encender/apagar la animación
|
|
15
|
+
* - color → semántico ('primary','success','danger','warning'...) o CSS color
|
|
16
|
+
* - colors → array para shimmer-border (multicolor)
|
|
17
|
+
* - intensity → 'subtle' | 'normal' | 'strong' (escala los keyframes)
|
|
18
|
+
* - duration → ms (por defecto depende del efecto)
|
|
19
|
+
* - border → grosor del shimmer-border en px
|
|
20
|
+
* - radius → radio del wrapper (para que rings/glow lo respeten)
|
|
21
|
+
* - count → número de iteraciones (default Infinity)
|
|
22
|
+
*
|
|
23
|
+
* ```vue
|
|
24
|
+
* <Attention effect="ping" color="danger" radius="full">
|
|
25
|
+
* <button class="bg-red-500 size-10 rounded-full"><IconBell /></button>
|
|
26
|
+
* </Attention>
|
|
27
|
+
*
|
|
28
|
+
* <Attention effect="shake" :duration="1500">
|
|
29
|
+
* <IconBell class="size-6 text-amber-500" />
|
|
30
|
+
* </Attention>
|
|
31
|
+
*
|
|
32
|
+
* <Attention effect="shimmer-border" :colors="['#3b82f6','#8b5cf6','#ec4899']" radius="md">
|
|
33
|
+
* <button class="px-4 py-2 bg-card">Probar PRO</button>
|
|
34
|
+
* </Attention>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const props = defineProps({
|
|
39
|
+
effect: { type: String, default: 'ping', validator: v => ['ping','pulse','shake','bounce','glow','shimmer-border'].includes(v) },
|
|
40
|
+
active: { type: Boolean, default: true },
|
|
41
|
+
color: { type: String, default: 'primary' },
|
|
42
|
+
colors: { type: Array, default: () => ['#3b82f6', '#8b5cf6', '#ec4899'] }, // para shimmer-border
|
|
43
|
+
intensity: { type: String, default: 'normal', validator: v => ['subtle','normal','strong'].includes(v) },
|
|
44
|
+
duration: { type: Number, default: 0 }, // 0 = default por efecto
|
|
45
|
+
border: { type: Number, default: 2 },
|
|
46
|
+
/** Radio del wrapper: 'inherit' | 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full' o un valor CSS. */
|
|
47
|
+
radius: { type: String, default: 'inherit' },
|
|
48
|
+
/** Iteraciones de la animación. Default 'infinite'. Pasá un número para limitar. */
|
|
49
|
+
count: { type: [Number, String], default: 'infinite' },
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const COLOR_MAP = {
|
|
53
|
+
primary: 'var(--color-primary)',
|
|
54
|
+
foreground: 'var(--color-foreground)',
|
|
55
|
+
muted: 'var(--color-muted-foreground)',
|
|
56
|
+
success: '#10b981',
|
|
57
|
+
danger: '#ef4444',
|
|
58
|
+
warning: '#f59e0b',
|
|
59
|
+
info: '#3b82f6',
|
|
60
|
+
emerald: '#10b981',
|
|
61
|
+
red: '#ef4444',
|
|
62
|
+
amber: '#f59e0b',
|
|
63
|
+
blue: '#3b82f6',
|
|
64
|
+
violet: '#8b5cf6',
|
|
65
|
+
pink: '#ec4899',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resolvedColor = computed(() => COLOR_MAP[props.color] ?? props.color)
|
|
69
|
+
|
|
70
|
+
const radiusMap = {
|
|
71
|
+
inherit: 'inherit',
|
|
72
|
+
none: '0',
|
|
73
|
+
sm: 'var(--radius-sm, 0.25rem)',
|
|
74
|
+
md: 'var(--radius-control, 0.5rem)',
|
|
75
|
+
lg: 'var(--radius-card, 0.75rem)',
|
|
76
|
+
xl: 'var(--radius-xl, 1rem)',
|
|
77
|
+
full: '9999px',
|
|
78
|
+
}
|
|
79
|
+
const resolvedRadius = computed(() => radiusMap[props.radius] ?? props.radius)
|
|
80
|
+
|
|
81
|
+
// Duración default por efecto — más lentas = más sofisticadas.
|
|
82
|
+
const defaultDuration = {
|
|
83
|
+
ping: 2400,
|
|
84
|
+
pulse: 2400,
|
|
85
|
+
shake: 1400,
|
|
86
|
+
bounce: 1400,
|
|
87
|
+
glow: 2800,
|
|
88
|
+
'shimmer-border': 4500,
|
|
89
|
+
}
|
|
90
|
+
const resolvedDuration = computed(() => props.duration > 0 ? props.duration : (defaultDuration[props.effect] ?? 1800))
|
|
91
|
+
|
|
92
|
+
const intensityScale = computed(() => ({ subtle: 0.6, normal: 1, strong: 1.5 }[props.intensity] ?? 1))
|
|
93
|
+
|
|
94
|
+
const cssVars = computed(() => ({
|
|
95
|
+
'--att-color': resolvedColor.value,
|
|
96
|
+
'--att-c1': props.colors[0] ?? resolvedColor.value,
|
|
97
|
+
'--att-c2': props.colors[1] ?? resolvedColor.value,
|
|
98
|
+
'--att-c3': props.colors[2] ?? resolvedColor.value,
|
|
99
|
+
'--att-c4': props.colors[3] ?? props.colors[0] ?? resolvedColor.value,
|
|
100
|
+
'--att-dur': resolvedDuration.value + 'ms',
|
|
101
|
+
'--att-radius': resolvedRadius.value,
|
|
102
|
+
'--att-border': props.border + 'px',
|
|
103
|
+
'--att-scale': intensityScale.value,
|
|
104
|
+
'--att-count': String(props.count),
|
|
105
|
+
}))
|
|
106
|
+
|
|
107
|
+
const hasRings = computed(() => props.active && props.effect === 'ping')
|
|
108
|
+
const hasGlow = computed(() => props.active && props.effect === 'glow')
|
|
109
|
+
const hasShimmer = computed(() => props.active && props.effect === 'shimmer-border')
|
|
110
|
+
const hasInlineEffect = computed(() => props.active && ['pulse','shake','bounce'].includes(props.effect))
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<template>
|
|
114
|
+
<span
|
|
115
|
+
class="att-wrap"
|
|
116
|
+
:class="active ? `att-mode-${effect}` : ''"
|
|
117
|
+
:style="cssVars"
|
|
118
|
+
>
|
|
119
|
+
<!-- Ondas de radar (ping) -->
|
|
120
|
+
<span v-if="hasRings" class="att-ring att-ring-a" aria-hidden="true" />
|
|
121
|
+
<span v-if="hasRings" class="att-ring att-ring-b" aria-hidden="true" />
|
|
122
|
+
|
|
123
|
+
<!-- Halo (glow) -->
|
|
124
|
+
<span v-if="hasGlow" class="att-glow" aria-hidden="true" />
|
|
125
|
+
|
|
126
|
+
<!-- Borde gradiente cónico (shimmer-border) -->
|
|
127
|
+
<span v-if="hasShimmer" class="att-shimmer" aria-hidden="true" />
|
|
128
|
+
|
|
129
|
+
<!-- Contenido — recibe la transformación cuando aplica -->
|
|
130
|
+
<span
|
|
131
|
+
class="att-content"
|
|
132
|
+
:class="hasInlineEffect ? `att-anim-${effect}` : ''"
|
|
133
|
+
>
|
|
134
|
+
<slot />
|
|
135
|
+
</span>
|
|
136
|
+
</span>
|
|
137
|
+
</template>
|
|
138
|
+
|
|
139
|
+
<style scoped>
|
|
140
|
+
/* Custom property registrada para que el ángulo del conic-gradient pueda
|
|
141
|
+
interpolar via animation (sin esto, no anima). */
|
|
142
|
+
@property --att-angle {
|
|
143
|
+
syntax: '<angle>';
|
|
144
|
+
inherits: false;
|
|
145
|
+
initial-value: 0deg;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.att-wrap {
|
|
149
|
+
position: relative;
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
isolation: isolate;
|
|
152
|
+
border-radius: var(--att-radius);
|
|
153
|
+
}
|
|
154
|
+
.att-content {
|
|
155
|
+
position: relative;
|
|
156
|
+
display: inline-flex;
|
|
157
|
+
z-index: 1;
|
|
158
|
+
border-radius: inherit;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ── PING — onda discreta ───────────────────────────────────────────── */
|
|
162
|
+
.att-ring {
|
|
163
|
+
position: absolute;
|
|
164
|
+
inset: 0;
|
|
165
|
+
border-radius: inherit;
|
|
166
|
+
border: 1px solid var(--att-color);
|
|
167
|
+
pointer-events: none;
|
|
168
|
+
z-index: 0;
|
|
169
|
+
opacity: 0;
|
|
170
|
+
animation: att-ping var(--att-dur) cubic-bezier(0, 0, 0.2, 1) var(--att-count);
|
|
171
|
+
}
|
|
172
|
+
.att-ring-b { animation-delay: calc(var(--att-dur) / 2); }
|
|
173
|
+
@keyframes att-ping {
|
|
174
|
+
0% { transform: scale(1); opacity: calc(0.5 * var(--att-scale)); }
|
|
175
|
+
100% { transform: scale(calc(1.35 + 0.25 * var(--att-scale))); opacity: 0; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── PULSE — respiración casi imperceptible ─────────────────────────── */
|
|
179
|
+
.att-anim-pulse {
|
|
180
|
+
animation: att-pulse var(--att-dur) ease-in-out var(--att-count);
|
|
181
|
+
}
|
|
182
|
+
@keyframes att-pulse {
|
|
183
|
+
0%, 100% { transform: scale(1); }
|
|
184
|
+
50% { transform: scale(calc(1 + 0.025 * var(--att-scale))); }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── SHAKE — leve oscilación ────────────────────────────────────────── */
|
|
188
|
+
.att-anim-shake {
|
|
189
|
+
animation: att-shake var(--att-dur) ease-in-out var(--att-count);
|
|
190
|
+
transform-origin: top center;
|
|
191
|
+
}
|
|
192
|
+
@keyframes att-shake {
|
|
193
|
+
0%, 60%, 100% { transform: rotate(0); }
|
|
194
|
+
10%, 30% { transform: rotate(calc(-7deg * var(--att-scale))); }
|
|
195
|
+
20%, 40% { transform: rotate(calc(7deg * var(--att-scale))); }
|
|
196
|
+
50% { transform: rotate(0); }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ── BOUNCE — rebote sutil ──────────────────────────────────────────── */
|
|
200
|
+
.att-anim-bounce {
|
|
201
|
+
animation: att-bounce var(--att-dur) cubic-bezier(.5,.05,.5,1) var(--att-count);
|
|
202
|
+
}
|
|
203
|
+
@keyframes att-bounce {
|
|
204
|
+
0%, 100% { transform: translateY(0); }
|
|
205
|
+
50% { transform: translateY(calc(-3px * var(--att-scale))); }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ── GLOW — halo difuso muy tenue ───────────────────────────────────── */
|
|
209
|
+
.att-glow {
|
|
210
|
+
position: absolute;
|
|
211
|
+
inset: calc(-2px * var(--att-scale));
|
|
212
|
+
border-radius: inherit;
|
|
213
|
+
background: var(--att-color);
|
|
214
|
+
filter: blur(calc(10px * var(--att-scale)));
|
|
215
|
+
pointer-events: none;
|
|
216
|
+
z-index: 0;
|
|
217
|
+
opacity: 0;
|
|
218
|
+
animation: att-glow var(--att-dur) ease-in-out var(--att-count);
|
|
219
|
+
}
|
|
220
|
+
@keyframes att-glow {
|
|
221
|
+
0%, 100% { opacity: calc(0.10 * var(--att-scale)); }
|
|
222
|
+
50% { opacity: calc(0.28 * var(--att-scale)); }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ── SHIMMER-BORDER — gradiente fluyendo por el borde ───────────────── */
|
|
226
|
+
/* Técnica: contenedor con conic-gradient + mask hueco para que sólo se
|
|
227
|
+
renderice el borde. El ángulo del gradiente se anima (no el elemento)
|
|
228
|
+
para que el flujo sea suave y los corners no se vean. */
|
|
229
|
+
.att-shimmer {
|
|
230
|
+
position: absolute;
|
|
231
|
+
inset: 0;
|
|
232
|
+
border-radius: inherit;
|
|
233
|
+
padding: var(--att-border);
|
|
234
|
+
background:
|
|
235
|
+
conic-gradient(
|
|
236
|
+
from var(--att-angle),
|
|
237
|
+
var(--att-c1),
|
|
238
|
+
var(--att-c2),
|
|
239
|
+
var(--att-c3),
|
|
240
|
+
var(--att-c4),
|
|
241
|
+
var(--att-c1)
|
|
242
|
+
);
|
|
243
|
+
/* Hueco: pinta sólo el padding (el borde). */
|
|
244
|
+
-webkit-mask:
|
|
245
|
+
linear-gradient(#000 0 0) content-box,
|
|
246
|
+
linear-gradient(#000 0 0);
|
|
247
|
+
-webkit-mask-composite: xor;
|
|
248
|
+
mask:
|
|
249
|
+
linear-gradient(#000 0 0) content-box,
|
|
250
|
+
linear-gradient(#000 0 0);
|
|
251
|
+
mask-composite: exclude;
|
|
252
|
+
pointer-events: none;
|
|
253
|
+
z-index: 2; /* Por encima del contenido para que se vea como borde. */
|
|
254
|
+
animation: att-angle var(--att-dur) linear var(--att-count);
|
|
255
|
+
}
|
|
256
|
+
@keyframes att-angle {
|
|
257
|
+
to { --att-angle: 360deg; }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* Reducción de animaciones por accesibilidad */
|
|
261
|
+
@media (prefers-reduced-motion: reduce) {
|
|
262
|
+
.att-ring,
|
|
263
|
+
.att-glow,
|
|
264
|
+
.att-shimmer,
|
|
265
|
+
.att-anim-pulse,
|
|
266
|
+
.att-anim-shake,
|
|
267
|
+
.att-anim-bounce { animation: none !important; }
|
|
268
|
+
}
|
|
269
|
+
</style>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import VueApexCharts from 'vue3-apexcharts'
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
/** Tipo de gráfico: line | bar | area | donut | pie | radialBar | scatter | heatmap */
|
|
7
|
+
type: { type: String, default: 'line' },
|
|
8
|
+
/** Series de datos — formato ApexCharts */
|
|
9
|
+
series: { type: Array, required: true },
|
|
10
|
+
/** Categorías para el eje X */
|
|
11
|
+
categories: { type: Array, default: () => [] },
|
|
12
|
+
/** Altura del gráfico en px o string CSS */
|
|
13
|
+
height: { type: [Number, String], default: 320 },
|
|
14
|
+
/** Opciones ApexCharts adicionales — se fusionan sobre las defaults */
|
|
15
|
+
options: { type: Object, default: () => ({}) },
|
|
16
|
+
/** Colores personalizados. Si se omite, usa los tokens --chart-* */
|
|
17
|
+
colors: { type: Array, default: null },
|
|
18
|
+
/** Muestra barra de herramientas */
|
|
19
|
+
toolbar: { type: Boolean, default: false },
|
|
20
|
+
/** Animación al montar */
|
|
21
|
+
animated: { type: Boolean, default: true },
|
|
22
|
+
/** Curva de líneas: smooth | straight | stepline */
|
|
23
|
+
curve: { type: String, default: 'smooth' },
|
|
24
|
+
/** Grosor de líneas (solo line/area) */
|
|
25
|
+
strokeWidth: { type: [Number, Array], default: 2 },
|
|
26
|
+
/** Mostrar dataLabels */
|
|
27
|
+
dataLabels: { type: Boolean, default: false },
|
|
28
|
+
/** Color de fondo de la zona — para area charts */
|
|
29
|
+
fillOpacity: { type: [Number, Array], default: undefined },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// ── Resolución de CSS vars a colores reales ───────────────────────────────────
|
|
33
|
+
const resolveCSSColor = (varName) => {
|
|
34
|
+
if (!process.client) return null
|
|
35
|
+
const el = document.createElement('span')
|
|
36
|
+
el.style.cssText = `display:none;color:var(${varName})`
|
|
37
|
+
document.body.appendChild(el)
|
|
38
|
+
const computed = getComputedStyle(el).color // siempre devuelve rgb(r,g,b)
|
|
39
|
+
document.body.removeChild(el)
|
|
40
|
+
// rgb(r, g, b) → #rrggbb
|
|
41
|
+
const match = computed.match(/\d+/g)
|
|
42
|
+
if (!match || match.length < 3) return null
|
|
43
|
+
return '#' + match.slice(0, 3).map(n => parseInt(n).toString(16).padStart(2, '0')).join('')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const resolveCSSVar = (varName) => {
|
|
47
|
+
if (!process.client) return ''
|
|
48
|
+
return getComputedStyle(document.documentElement).getPropertyValue(varName).trim()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Dark mode reactive ────────────────────────────────────────────────────────
|
|
52
|
+
const isDark = useState('isDark', () => false)
|
|
53
|
+
|
|
54
|
+
// ── Paleta dinámica ───────────────────────────────────────────────────────────
|
|
55
|
+
const CHART_VARS = [
|
|
56
|
+
'--chart-primary',
|
|
57
|
+
'--chart-2', '--chart-3', '--chart-4', '--chart-5',
|
|
58
|
+
'--chart-6', '--chart-7',
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const resolvedColors = computed(() => {
|
|
62
|
+
// isDark.value registra reactividad; los colores se re-resuelven al cambiar el tema
|
|
63
|
+
void isDark.value
|
|
64
|
+
if (props.colors) return props.colors
|
|
65
|
+
return CHART_VARS.map(v => resolveCSSColor(v)).filter(Boolean)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
// ── Opciones base ─────────────────────────────────────────────────────────────
|
|
69
|
+
const baseOptions = computed(() => {
|
|
70
|
+
void isDark.value // trigger reactivity on dark mode toggle
|
|
71
|
+
const mutedFg = resolveCSSColor('--color-muted-foreground') ?? '#9ca3af'
|
|
72
|
+
const cardLine = resolveCSSColor('--color-card-line') ?? '#e5e7eb'
|
|
73
|
+
const tooltipBg = resolveCSSColor('--color-card') ?? '#ffffff'
|
|
74
|
+
const tooltipFg = resolveCSSColor('--color-foreground') ?? '#111827'
|
|
75
|
+
|
|
76
|
+
const isDonutLike = ['donut', 'pie', 'radialBar'].includes(props.type)
|
|
77
|
+
|
|
78
|
+
const opts = {
|
|
79
|
+
chart: {
|
|
80
|
+
background: 'transparent',
|
|
81
|
+
toolbar: { show: props.toolbar },
|
|
82
|
+
fontFamily: 'inherit',
|
|
83
|
+
foreColor: mutedFg,
|
|
84
|
+
animations: { enabled: props.animated },
|
|
85
|
+
redrawOnWindowResize: true,
|
|
86
|
+
},
|
|
87
|
+
colors: resolvedColors.value,
|
|
88
|
+
|
|
89
|
+
stroke: isDonutLike ? undefined : {
|
|
90
|
+
curve: props.curve,
|
|
91
|
+
width: props.strokeWidth,
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
dataLabels: {
|
|
95
|
+
enabled: props.dataLabels,
|
|
96
|
+
style: { fontFamily: 'inherit', fontSize: '11px' },
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
...(props.fillOpacity !== undefined ? {
|
|
100
|
+
fill: { opacity: props.fillOpacity },
|
|
101
|
+
} : {}),
|
|
102
|
+
|
|
103
|
+
xaxis: isDonutLike ? undefined : {
|
|
104
|
+
categories: props.categories,
|
|
105
|
+
axisBorder: { show: false },
|
|
106
|
+
axisTicks: { show: false },
|
|
107
|
+
labels: {
|
|
108
|
+
style: { colors: mutedFg, fontFamily: 'inherit', fontSize: '12px' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
yaxis: isDonutLike ? undefined : {
|
|
113
|
+
labels: {
|
|
114
|
+
style: { colors: mutedFg, fontFamily: 'inherit', fontSize: '12px' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
grid: isDonutLike ? { show: false } : {
|
|
119
|
+
borderColor: cardLine,
|
|
120
|
+
strokeDashArray: 4,
|
|
121
|
+
xaxis: { lines: { show: false } },
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
legend: {
|
|
125
|
+
fontFamily: 'inherit',
|
|
126
|
+
fontSize: '13px',
|
|
127
|
+
labels: { colors: tooltipFg },
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
tooltip: {
|
|
131
|
+
theme: isDark.value ? 'dark' : 'light',
|
|
132
|
+
style: { fontFamily: 'inherit', fontSize: '12px' },
|
|
133
|
+
x: { show: !isDonutLike },
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
plotOptions: props.type === 'bar' ? {
|
|
137
|
+
bar: { borderRadius: 4, columnWidth: '60%' },
|
|
138
|
+
} : props.type === 'radialBar' ? {
|
|
139
|
+
radialBar: {
|
|
140
|
+
hollow: { size: '60%' },
|
|
141
|
+
dataLabels: {
|
|
142
|
+
name: { color: tooltipFg, fontFamily: 'inherit' },
|
|
143
|
+
value: { color: tooltipFg, fontFamily: 'inherit', fontSize: '20px' },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
} : undefined,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Limpiar undefined para que ApexCharts no se confunda
|
|
150
|
+
return JSON.parse(JSON.stringify(opts))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// ── Merge profundo de opciones ────────────────────────────────────────────────
|
|
154
|
+
const mergedOptions = computed(() => deepMerge(baseOptions.value, props.options))
|
|
155
|
+
|
|
156
|
+
function deepMerge(base, override) {
|
|
157
|
+
const result = { ...base }
|
|
158
|
+
for (const key of Object.keys(override ?? {})) {
|
|
159
|
+
if (
|
|
160
|
+
override[key] !== null &&
|
|
161
|
+
typeof override[key] === 'object' &&
|
|
162
|
+
!Array.isArray(override[key]) &&
|
|
163
|
+
base[key] !== null &&
|
|
164
|
+
typeof base[key] === 'object' &&
|
|
165
|
+
!Array.isArray(base[key])
|
|
166
|
+
) {
|
|
167
|
+
result[key] = deepMerge(base[key], override[key])
|
|
168
|
+
} else {
|
|
169
|
+
result[key] = override[key]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result
|
|
173
|
+
}
|
|
174
|
+
</script>
|
|
175
|
+
|
|
176
|
+
<template>
|
|
177
|
+
<ClientOnly>
|
|
178
|
+
<VueApexCharts
|
|
179
|
+
:type="type"
|
|
180
|
+
:height="height"
|
|
181
|
+
:series="series"
|
|
182
|
+
:options="mergedOptions"
|
|
183
|
+
/>
|
|
184
|
+
<template #fallback>
|
|
185
|
+
<div
|
|
186
|
+
:style="{ height: typeof height === 'number' ? height + 'px' : height }"
|
|
187
|
+
class="flex items-center justify-center bg-surface rounded-card"
|
|
188
|
+
>
|
|
189
|
+
<div class="w-6 h-6 rounded-full border-2 border-primary border-t-transparent animate-spin" />
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
|
192
|
+
</ClientOnly>
|
|
193
|
+
</template>
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* <Container> — Card / panel reutilizable que sigue los tokens del DS.
|
|
4
|
+
*
|
|
5
|
+
* Usado para contenido estático: settings, dashboards, KPI tiles, info boxes,
|
|
6
|
+
* formularios agrupados. NO para Tree/Table/Kanban (esos manejan su propio
|
|
7
|
+
* layout interno complejo).
|
|
8
|
+
*
|
|
9
|
+
* Slots:
|
|
10
|
+
* - default — body
|
|
11
|
+
* - header — reemplaza el header entero (si necesitás algo custom)
|
|
12
|
+
* - actions — botones a la derecha del título
|
|
13
|
+
* - footer — pie con border-top
|
|
14
|
+
*
|
|
15
|
+
* Props:
|
|
16
|
+
* - title / subtitle — texto del header automático
|
|
17
|
+
* - bordered — true = border + rounded-card (default)
|
|
18
|
+
* - padding — 'none' | 'sm' | 'md' (default) | 'lg' del body
|
|
19
|
+
* - size — 'sm' | 'md' | 'lg' | 'fit' (default) — min-height baseline
|
|
20
|
+
* - tone — 'default' | 'muted' | 'primary' | 'destructive' | 'emerald' | 'amber'
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
title: { type: String, default: '' },
|
|
25
|
+
subtitle: { type: String, default: '' },
|
|
26
|
+
bordered: { type: Boolean, default: true },
|
|
27
|
+
padding: { type: String, default: 'md' },
|
|
28
|
+
size: { type: String, default: 'fit' },
|
|
29
|
+
tone: { type: String, default: 'default' },
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const slots = useSlots()
|
|
33
|
+
|
|
34
|
+
const hasAutoHeader = computed(() => !!(props.title || props.subtitle || slots.actions))
|
|
35
|
+
const hasHeaderSlot = computed(() => !!slots.header)
|
|
36
|
+
const showHeader = computed(() => hasHeaderSlot.value || hasAutoHeader.value)
|
|
37
|
+
|
|
38
|
+
// ─── Padding del body ────────────────────────────────────────────────────────
|
|
39
|
+
const paddingClass = computed(() => ({
|
|
40
|
+
none: '',
|
|
41
|
+
sm: 'p-3',
|
|
42
|
+
md: 'p-5',
|
|
43
|
+
lg: 'p-7',
|
|
44
|
+
}[props.padding] ?? 'p-5'))
|
|
45
|
+
|
|
46
|
+
// ─── Size (min-height baseline) ──────────────────────────────────────────────
|
|
47
|
+
const sizeStyle = computed(() => {
|
|
48
|
+
switch (props.size) {
|
|
49
|
+
case 'sm': return { minHeight: '8rem' }
|
|
50
|
+
case 'md': return { minHeight: '14rem' }
|
|
51
|
+
case 'lg': return { minHeight: '24rem' }
|
|
52
|
+
case 'fit':
|
|
53
|
+
default: return {}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// ─── Tone (color del border + bg sutil) ──────────────────────────────────────
|
|
58
|
+
const toneClasses = computed(() => {
|
|
59
|
+
if (!props.bordered) return 'bg-card'
|
|
60
|
+
switch (props.tone) {
|
|
61
|
+
case 'muted': return 'bg-muted/30 border-card-line'
|
|
62
|
+
case 'primary': return 'bg-primary/5 border-primary/20'
|
|
63
|
+
case 'destructive': return 'bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-900/50'
|
|
64
|
+
case 'emerald': return 'bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-900/50'
|
|
65
|
+
case 'amber': return 'bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-900/50'
|
|
66
|
+
case 'default':
|
|
67
|
+
default: return 'bg-card border-card-line'
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<div
|
|
74
|
+
:class="[
|
|
75
|
+
'flex flex-col',
|
|
76
|
+
toneClasses,
|
|
77
|
+
bordered ? 'border rounded-card overflow-hidden' : '',
|
|
78
|
+
]"
|
|
79
|
+
:style="sizeStyle"
|
|
80
|
+
>
|
|
81
|
+
<!-- Header -->
|
|
82
|
+
<div
|
|
83
|
+
v-if="showHeader"
|
|
84
|
+
:class="[
|
|
85
|
+
'shrink-0 flex items-center gap-3 px-5 py-4',
|
|
86
|
+
bordered ? 'border-b border-card-line' : '',
|
|
87
|
+
]"
|
|
88
|
+
>
|
|
89
|
+
<slot name="header">
|
|
90
|
+
<div class="flex-1 min-w-0">
|
|
91
|
+
<h3 v-if="title" class="text-sm font-semibold text-foreground truncate">{{ title }}</h3>
|
|
92
|
+
<p v-if="subtitle" class="text-xs text-muted-foreground mt-0.5 truncate">{{ subtitle }}</p>
|
|
93
|
+
</div>
|
|
94
|
+
<div v-if="$slots.actions" class="shrink-0 flex items-center gap-1">
|
|
95
|
+
<slot name="actions" />
|
|
96
|
+
</div>
|
|
97
|
+
</slot>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Body -->
|
|
101
|
+
<div :class="['flex-1 min-h-0', paddingClass]">
|
|
102
|
+
<slot />
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Footer -->
|
|
106
|
+
<div
|
|
107
|
+
v-if="$slots.footer"
|
|
108
|
+
:class="[
|
|
109
|
+
'shrink-0 px-5 py-3',
|
|
110
|
+
bordered ? 'border-t border-card-line' : '',
|
|
111
|
+
]"
|
|
112
|
+
>
|
|
113
|
+
<slot name="footer" />
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { IconCopy, IconCheck } from '@tabler/icons-vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* <CopyButton :value="..." /> — Botón con feedback visual de copia.
|
|
6
|
+
*
|
|
7
|
+
* Variants:
|
|
8
|
+
* - 'icon' (default) — solo el icono
|
|
9
|
+
* - 'inline' — input readonly con icono al lado (estilo Preline clipboard)
|
|
10
|
+
* - 'button' — botón ancho con label "Copiar"
|
|
11
|
+
*
|
|
12
|
+
* ```vue
|
|
13
|
+
* <CopyButton :value="user.id" />
|
|
14
|
+
* <CopyButton :value="apiKey" variant="inline" />
|
|
15
|
+
* <CopyButton :value="link" variant="button" label="Copiar enlace" />
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
value: { type: [String, Number], default: '' },
|
|
20
|
+
variant: { type: String, default: 'icon', validator: v => ['icon','inline','button'].includes(v) },
|
|
21
|
+
label: { type: String, default: 'Copiar' },
|
|
22
|
+
labelCopied: { type: String, default: '¡Copiado!' },
|
|
23
|
+
size: { type: String, default: 'sm', validator: v => ['xs','sm','md'].includes(v) },
|
|
24
|
+
/** Solo para variant="inline" — placeholder cuando el value está vacío. */
|
|
25
|
+
placeholder: { type: String, default: '' },
|
|
26
|
+
/** Solo para variant="inline" — oculta el value (útil para tokens/passwords). */
|
|
27
|
+
masked: { type: Boolean, default: false },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits(['copied'])
|
|
31
|
+
|
|
32
|
+
const { copy, copied } = useClipboard({ timeout: 1500 })
|
|
33
|
+
|
|
34
|
+
const onCopy = async () => {
|
|
35
|
+
if (await copy(props.value)) emit('copied', props.value)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const iconSize = computed(() => ({ xs: 12, sm: 14, md: 16 }[props.size]))
|
|
39
|
+
const btnSize = computed(() => ({ xs: 'size-6', sm: 'size-7', md: 'size-8' }[props.size]))
|
|
40
|
+
|
|
41
|
+
const displayValue = computed(() => {
|
|
42
|
+
if (!props.value) return ''
|
|
43
|
+
if (!props.masked) return String(props.value)
|
|
44
|
+
return String(props.value).replace(/./g, '•').slice(0, 32)
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<!-- Icon-only -->
|
|
50
|
+
<button
|
|
51
|
+
v-if="variant === 'icon'"
|
|
52
|
+
type="button"
|
|
53
|
+
:title="copied ? labelCopied : label"
|
|
54
|
+
:class="[btnSize, 'inline-flex items-center justify-center rounded-control transition-colors',
|
|
55
|
+
copied ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
56
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted-hover']"
|
|
57
|
+
@click="onCopy"
|
|
58
|
+
>
|
|
59
|
+
<IconCheck v-if="copied" :size="iconSize" :stroke="2" />
|
|
60
|
+
<IconCopy v-else :size="iconSize" :stroke="1.75" />
|
|
61
|
+
</button>
|
|
62
|
+
|
|
63
|
+
<!-- Inline input + icon (Preline clipboard style) -->
|
|
64
|
+
<div
|
|
65
|
+
v-else-if="variant === 'inline'"
|
|
66
|
+
class="inline-flex items-center gap-2 w-full innertia-field !pr-1 !py-1 cursor-pointer"
|
|
67
|
+
@click="onCopy"
|
|
68
|
+
>
|
|
69
|
+
<span class="flex-1 truncate text-sm text-foreground font-mono">
|
|
70
|
+
{{ displayValue || placeholder }}
|
|
71
|
+
</span>
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
:class="['shrink-0 inline-flex items-center justify-center size-7 rounded-control transition-colors',
|
|
75
|
+
copied ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
76
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted-hover']"
|
|
77
|
+
@click.stop="onCopy"
|
|
78
|
+
>
|
|
79
|
+
<IconCheck v-if="copied" :size="14" :stroke="2" />
|
|
80
|
+
<IconCopy v-else :size="14" :stroke="1.75" />
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Full button -->
|
|
85
|
+
<button
|
|
86
|
+
v-else
|
|
87
|
+
type="button"
|
|
88
|
+
:class="['inline-flex items-center gap-1.5 px-3 py-1.5 rounded-control text-xs font-medium transition-colors border',
|
|
89
|
+
copied
|
|
90
|
+
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/30'
|
|
91
|
+
: 'bg-card text-foreground border-card-line hover:bg-muted-hover']"
|
|
92
|
+
@click="onCopy"
|
|
93
|
+
>
|
|
94
|
+
<IconCheck v-if="copied" :size="14" :stroke="2" />
|
|
95
|
+
<IconCopy v-else :size="14" :stroke="1.75" />
|
|
96
|
+
{{ copied ? labelCopied : label }}
|
|
97
|
+
</button>
|
|
98
|
+
</template>
|