feat(studio): un solo set de iconos en vez de emojis mezclados con símbolos
En la misma pantalla convivían dos lenguajes: emojis a color (🌙 📊 🧠 ✍️ 📄 🌐 🔁) y símbolos tipográficos monocromos (✎ ✕ ← ↻ ⚠ ⬇ ✓). Eso se lee como descuido, y encima los emojis los dibuja el sistema operativo — el mismo panel se ve distinto en Mac, en Windows y en Android, sin que podamos hacer nada. Ahora hay un set propio de iconos SVG de trazo: mismo grosor de línea, misma caja, y heredan currentColor, así que toman el color del texto que los rodea y funcionan igual en tema claro y oscuro. Al lado uno de otro tienen el mismo peso visual, que es lo que hacía falta. El emoji suelto de la pantalla de inicio pasa a ser Umi, que ya es el personaje del producto. Un detalle de implementación que costó dos intentos: los iconos NO pueden declararse como cadenas de HTML para v-html. Dentro de un <svg>, v-html parsea como HTML y los <path> quedan sin el namespace de SVG: aparecen en el DOM pero no dibujan nada, así que la primera versión se veía perfecta en el código y en blanco en pantalla. Van declarados como pares [etiqueta, atributos] para que Vue los cree con el namespace correcto. Y AgenteDetail usaba cinco iconos sin importar el componente. Verificado ahora sobre todos los archivos: cada componente usado está importado donde se usa. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002b75e71b
commit
ee9bb2cfb7
@@ -3,6 +3,7 @@ import Sidebar from './components/Sidebar.vue'
|
||||
import { tema, alternarTema } from './lib/tema.js'
|
||||
import { menuAbierto } from './lib/ui.js'
|
||||
import DespertarUmind from './components/DespertarUmind.vue'
|
||||
import UiIcono from './components/ui/UiIcono.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -27,7 +28,7 @@ import DespertarUmind from './components/DespertarUmind.vue'
|
||||
:title="tema === 'oscuro' ? 'Cambiar a claro' : 'Cambiar a oscuro'"
|
||||
@click="alternarTema"
|
||||
>
|
||||
<span class="text-base leading-none">{{ tema === 'oscuro' ? '☀️' : '🌙' }}</span>
|
||||
<UiIcono :nombre="tema === 'oscuro' ? 'sol' : 'luna'" :tam="17" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="flex-1 max-w-5xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8 animate-aparecer">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import UiIcono from './ui/UiIcono.vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { api } from '../lib/api.js'
|
||||
@@ -191,14 +192,14 @@ onMounted(() => {
|
||||
title="Editar"
|
||||
@click="editarTenant(t)"
|
||||
>
|
||||
✎
|
||||
<UiIcono nombre="lapiz" :tam="14" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1 text-tenue hover:text-red-600"
|
||||
title="Eliminar"
|
||||
@click="eliminar(t)"
|
||||
>
|
||||
✕
|
||||
<UiIcono nombre="cerrar" :tam="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
<script setup>
|
||||
// Un solo set de iconos para todo uMind.
|
||||
//
|
||||
// Antes convivían emojis a color (🌙 📊 🧠 ✍️) con símbolos tipográficos
|
||||
// monocromos (✎ ✕ ← ↻). Dos lenguajes distintos en la misma pantalla se leen
|
||||
// como descuido, y encima los emojis los dibuja el sistema operativo: el mismo
|
||||
// panel se ve distinto en Mac, en Windows y en Android, sin que podamos hacer
|
||||
// nada.
|
||||
//
|
||||
// Estos son SVG de trazo que heredan currentColor, así que toman el color del
|
||||
// texto que los rodea y funcionan igual en tema claro y oscuro. Mismo grosor de
|
||||
// línea y misma caja en todos: al lado uno de otro tienen el mismo peso visual.
|
||||
defineProps({
|
||||
nombre: { type: String, required: true },
|
||||
tam: { type: [Number, String], default: 16 },
|
||||
})
|
||||
|
||||
// Cada icono es una lista de [etiqueta, atributos]. No es una cadena de HTML a
|
||||
// propósito: v-html dentro de un <svg> parsea como HTML y los <path> quedan sin
|
||||
// el namespace de SVG — se insertan en el DOM pero no dibujan nada. Declarados
|
||||
// así, Vue los crea con el namespace correcto porque ve el <svg> padre.
|
||||
const ICONOS = {
|
||||
luna: [['path', { d: 'M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5Z' }]],
|
||||
sol: [
|
||||
['circle', { cx: 12, cy: 12, r: 4 }],
|
||||
['path', { d: 'M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4' }],
|
||||
],
|
||||
lapiz: [
|
||||
['path', { d: 'M12 20h9' }],
|
||||
['path', { d: 'M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z' }],
|
||||
],
|
||||
cerrar: [['path', { d: 'M18 6 6 18M6 6l12 12' }]],
|
||||
atras: [['path', { d: 'M19 12H5M12 19l-7-7 7-7' }]],
|
||||
grafico: [
|
||||
['path', { d: 'M3 3v18h18' }],
|
||||
['path', { d: 'M7 15v3M12 10v8M17 6v12' }],
|
||||
],
|
||||
cerebro: [
|
||||
['path', { d: 'M9.5 3A2.5 2.5 0 0 0 7 5.5v.2A2.7 2.7 0 0 0 5 8.3c0 .8.3 1.5.9 2A2.8 2.8 0 0 0 5 12.5c0 1 .5 1.9 1.3 2.4-.2.4-.3.8-.3 1.2A2.9 2.9 0 0 0 9 19a2.5 2.5 0 0 0 3 2V3.8A2.5 2.5 0 0 0 9.5 3Z' }],
|
||||
['path', { d: 'M14.5 3A2.5 2.5 0 0 1 17 5.5v.2a2.7 2.7 0 0 1 2 2.6c0 .8-.3 1.5-.9 2A2.8 2.8 0 0 1 19 12.5c0 1-.5 1.9-1.3 2.4.2.4.3.8.3 1.2A2.9 2.9 0 0 1 15 19a2.5 2.5 0 0 1-3 2' }],
|
||||
],
|
||||
imagen: [
|
||||
['rect', { x: 3, y: 3, width: 18, height: 18, rx: 2 }],
|
||||
['circle', { cx: 8.5, cy: 8.5, r: 1.5 }],
|
||||
['path', { d: 'm21 15-5-5L5 21' }],
|
||||
],
|
||||
microfono: [
|
||||
['rect', { x: 9, y: 2, width: 6, height: 12, rx: 3 }],
|
||||
['path', { d: 'M5 10a7 7 0 0 0 14 0M12 17v5' }],
|
||||
],
|
||||
escribir: [
|
||||
['path', { d: 'M3 21h18' }],
|
||||
['path', { d: 'M5 17 17.5 4.5a2.1 2.1 0 0 1 3 3L8 20l-4 1Z' }],
|
||||
],
|
||||
archivo: [
|
||||
['path', { d: 'M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7Z' }],
|
||||
['path', { d: 'M14 2v5h5' }],
|
||||
],
|
||||
sitio: [
|
||||
['circle', { cx: 12, cy: 12, r: 9 }],
|
||||
['path', { d: 'M3 12h18M12 3a15 15 0 0 1 0 18 15 15 0 0 1 0-18Z' }],
|
||||
],
|
||||
refrescar: [
|
||||
['path', { d: 'M21 12a9 9 0 1 1-3-6.7' }],
|
||||
['path', { d: 'M21 4v5h-5' }],
|
||||
],
|
||||
descargar: [
|
||||
['path', { d: 'M12 3v12' }],
|
||||
['path', { d: 'm7 11 5 5 5-5' }],
|
||||
['path', { d: 'M4 21h16' }],
|
||||
],
|
||||
alerta: [
|
||||
['path', { d: 'M12 9v4M12 17h.01' }],
|
||||
['path', { d: 'M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z' }],
|
||||
],
|
||||
ok: [['path', { d: 'M20 6 9 17l-5-5' }]],
|
||||
copiar: [
|
||||
['rect', { x: 9, y: 9, width: 12, height: 12, rx: 2 }],
|
||||
['path', { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' }],
|
||||
],
|
||||
mensaje: [['path', { d: 'M21 11.5a8.4 8.4 0 0 1-9 8.4 8.5 8.5 0 0 1-3.9-.9L3 21l1.9-5.1A8.4 8.4 0 0 1 4 11.5a8.5 8.5 0 0 1 8.5-8.5A8.4 8.4 0 0 1 21 11.5Z' }]],
|
||||
auto: [
|
||||
['path', { d: 'M17 2.1 21 6l-4 3.9' }],
|
||||
['path', { d: 'M3 11V9a4 4 0 0 1 4-4h14' }],
|
||||
['path', { d: 'M7 21.9 3 18l4-3.9' }],
|
||||
['path', { d: 'M21 13v2a4 4 0 0 1-4 4H3' }],
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:width="tam"
|
||||
:height="tam"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="inline-block shrink-0 align-[-0.15em]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<component
|
||||
v-for="(el, i) in ICONOS[nombre] || []"
|
||||
:key="i"
|
||||
:is="el[0]"
|
||||
v-bind="el[1]"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup>
|
||||
import UiIcono from './UiIcono.vue'
|
||||
defineProps({ titulo: { type: String, default: '' }, ancho: { type: String, default: 'max-w-lg' } })
|
||||
const emit = defineEmits(['cerrar'])
|
||||
</script>
|
||||
@@ -12,7 +13,7 @@ const emit = defineEmits(['cerrar'])
|
||||
<div :class="['card w-full p-6 max-h-[90vh] overflow-y-auto animate-escalar shadow-2xl', ancho]">
|
||||
<div v-if="titulo" class="flex items-center justify-between mb-4">
|
||||
<h2 class="font-semibold text-texto">{{ titulo }}</h2>
|
||||
<button class="btn-ghost !px-2 !py-1" type="button" @click="emit('cerrar')">✕</button>
|
||||
<button class="btn-ghost !px-2 !py-1" type="button" @click="emit('cerrar')"><UiIcono nombre="cerrar" :tam="15" /></button>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { api } from '../lib/api.js'
|
||||
import { apiUmind, contexto } from '../lib/contexto.js'
|
||||
import UiEmptyState from '../components/ui/UiEmptyState.vue'
|
||||
import UiMascota from '../components/ui/UiMascota.vue'
|
||||
import UiIcono from '../components/ui/UiIcono.vue'
|
||||
|
||||
const props = defineProps({
|
||||
tenantId: { type: String, required: true },
|
||||
@@ -414,7 +415,7 @@ async function enviarChatPrueba() {
|
||||
const r = await api.post(apiUmind('/umind/chat'), { agente_id: agenteIdNum.value, session_id: chatSessionId, mensaje: texto })
|
||||
chatMensajes.value.push({ role: 'assistant', content: r.respuesta })
|
||||
} catch (e) {
|
||||
chatMensajes.value.push({ role: 'assistant', content: `⚠️ ${e.message}` })
|
||||
chatMensajes.value.push({ role: 'assistant', content: `⚠ ${e.message}` })
|
||||
} finally {
|
||||
chatEnviando.value = false
|
||||
}
|
||||
@@ -507,7 +508,7 @@ watch(
|
||||
<!-- Un archivo que se reenvía sirve para justificar el gasto puertas
|
||||
adentro; un panel al que hay que entrar, no. -->
|
||||
<a :href="urlReporte" class="btn-ghost shrink-0" title="Conversaciones y consumo del mes en Excel">
|
||||
⬇ Reporte del mes
|
||||
<UiIcono nombre="descargar" :tam="15" /> Reporte del mes
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -532,12 +533,12 @@ watch(
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="flex gap-1 mb-3">
|
||||
<button
|
||||
v-for="[k, l] in [['texto', '✍️ Escribir'], ['archivo', '📄 Subir archivo'], ['url', '🌐 Sitio web']]"
|
||||
v-for="[k, ico, l] in [['texto', 'escribir', 'Escribir'], ['archivo', 'archivo', 'Subir archivo'], ['url', 'sitio', 'Sitio web']]"
|
||||
:key="k"
|
||||
class="px-3 py-1.5 rounded-lg text-sm"
|
||||
class="px-3 py-1.5 rounded-lg text-sm inline-flex items-center gap-1.5"
|
||||
:class="fuenteNueva === k ? 'bg-brand text-white font-medium' : 'text-tenue hover:text-texto'"
|
||||
@click="fuenteNueva = k"
|
||||
>{{ l }}</button>
|
||||
><UiIcono :nombre="ico" :tam="15" /> {{ l }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Lo que el dueño sabe y no está escrito en ningún lado. -->
|
||||
@@ -616,7 +617,7 @@ watch(
|
||||
class="text-xs text-tenue hover:text-texto"
|
||||
:title="d.auto_actualizar ? 'Dejar de releer sola' : 'Releer el sitio cada semana'"
|
||||
@click="alternarAuto(d)"
|
||||
>{{ d.auto_actualizar ? '🔁 auto' : '↻ manual' }}</button>
|
||||
><UiIcono :nombre="d.auto_actualizar ? 'auto' : 'refrescar'" :tam="13" class="mr-1" />{{ d.auto_actualizar ? 'auto' : 'manual' }}</button>
|
||||
<!-- Solo lo que tiene texto propio guardado: una URL se rehace
|
||||
crawleando, editarla a mano se perdería en la próxima pasada. -->
|
||||
<button
|
||||
@@ -740,7 +741,7 @@ watch(
|
||||
<label class="label flex items-center gap-1">
|
||||
<input v-model="p.requerido" type="checkbox" /> req.
|
||||
</label>
|
||||
<button type="button" class="text-red-400 text-xs" @click="quitarParametro(i)">✕</button>
|
||||
<button type="button" class="text-red-400 text-xs" @click="quitarParametro(i)"><UiIcono nombre="cerrar" :tam="13" /></button>
|
||||
</div>
|
||||
<p v-if="toolForm.parametros.length === 0" class="text-xs text-gray-400">Sin parámetros.</p>
|
||||
</div>
|
||||
@@ -788,7 +789,8 @@ watch(
|
||||
:class="widgetCopiado ? 'bg-green-600 text-white' : 'bg-brand hover:bg-brand-dark text-white'"
|
||||
@click="copiarWidget"
|
||||
>
|
||||
{{ widgetCopiado ? '✓ Copiado' : 'Copiar código' }}
|
||||
<UiIcono v-if="widgetCopiado" nombre="ok" :tam="14" />
|
||||
{{ widgetCopiado ? 'Copiado' : 'Copiar código' }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="label mb-2">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, watch } from 'vue'
|
||||
import { api } from '../lib/api.js'
|
||||
import { apiUmind } from '../lib/contexto.js'
|
||||
import UiEmptyState from '../components/ui/UiEmptyState.vue'
|
||||
import UiIcono from '../components/ui/UiIcono.vue'
|
||||
|
||||
const props = defineProps({ id: { type: String, required: true } })
|
||||
|
||||
@@ -122,7 +123,7 @@ async function borrar(c) {
|
||||
<div>
|
||||
<div class="flex items-start justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<router-link :to="`/tenants/${id}`" class="text-sm text-tenue hover:text-texto">← Agentes</router-link>
|
||||
<router-link :to="`/tenants/${id}`" class="text-sm text-tenue hover:text-texto inline-flex items-center gap-1.5"><UiIcono nombre="atras" :tam="14" /> Agentes</router-link>
|
||||
<h1 class="text-xl font-semibold text-texto mt-1">Tu cuenta de IA</h1>
|
||||
<p class="text-sm text-tenue mt-1 max-w-xl">
|
||||
Podés usar tu propia cuenta de OpenAI, Claude o Gemini en vez de la nuestra.
|
||||
|
||||
@@ -68,7 +68,7 @@ onMounted(irAlAgenteSiEsUnoSolo)
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col items-center justify-center text-center py-24">
|
||||
<div class="text-4xl mb-4">💬</div>
|
||||
<UiMascota estado="normal" :tam="64" class="mx-auto mb-4 text-brand" />
|
||||
<h1 class="text-lg font-medium text-texto">
|
||||
{{ contexto.esPortal ? 'Elegí tu espacio de la izquierda' : 'Elegí un espacio de la izquierda' }}
|
||||
</h1>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router'
|
||||
import { api } from '../lib/api.js'
|
||||
import { apiUmind, contexto } from '../lib/contexto.js'
|
||||
import UiEmptyState from '../components/ui/UiEmptyState.vue'
|
||||
import UiIcono from '../components/ui/UiIcono.vue'
|
||||
|
||||
const props = defineProps({ id: { type: String, required: true } })
|
||||
const tenantId = computed(() => Number(props.id))
|
||||
@@ -168,7 +169,7 @@ watch(() => props.id, cargar, { immediate: true })
|
||||
<p class="text-xs text-tenue mt-1">{{ tenant.dominios_permitidos || 'sin dominios configurados' }}</p>
|
||||
</div>
|
||||
<router-link :to="`/tenants/${tenantId ?? id}/ia`" class="btn-ghost">Tu IA</router-link>
|
||||
<router-link :to="`/tenants/${tenantId ?? id}/uso`" class="btn-ghost">📊 Consumo</router-link>
|
||||
<router-link :to="`/tenants/${tenantId ?? id}/uso`" class="btn-ghost inline-flex items-center gap-1.5"><UiIcono nombre="grafico" /> Consumo</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -243,13 +244,13 @@ watch(() => props.id, cargar, { immediate: true })
|
||||
</div>
|
||||
|
||||
<div class="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||
<button class="p-1 text-tenue hover:text-texto" title="Editar" @click.prevent.stop="editarAgente(a)">✎</button>
|
||||
<button class="p-1 text-tenue hover:text-texto" title="Editar" @click.prevent.stop="editarAgente(a)"><UiIcono nombre="lapiz" :tam="14" /></button>
|
||||
<button
|
||||
class="p-1 text-tenue hover:text-texto"
|
||||
title="Duplicar: copia el conocimiento y las herramientas a un agente nuevo"
|
||||
@click.prevent.stop="duplicarAgente(a)"
|
||||
>⧉</button>
|
||||
<button class="p-1 text-tenue hover:text-red-600" title="Eliminar" @click.prevent.stop="eliminarAgente(a)">✕</button>
|
||||
><UiIcono nombre="copiar" :tam="14" /></button>
|
||||
<button class="p-1 text-tenue hover:text-red-600" title="Eliminar" @click.prevent.stop="eliminarAgente(a)"><UiIcono nombre="cerrar" :tam="14" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { api } from '../lib/api.js'
|
||||
import { apiUmind } from '../lib/contexto.js'
|
||||
import UiEmptyState from '../components/ui/UiEmptyState.vue'
|
||||
import UiIcono from '../components/ui/UiIcono.vue'
|
||||
|
||||
const props = defineProps({ id: { type: String, required: true } })
|
||||
|
||||
@@ -16,11 +17,11 @@ const desde = ref(new Date(hoy.getFullYear(), hoy.getMonth(), 1).toISOString().s
|
||||
const hasta = ref(hoy.toISOString().slice(0, 10))
|
||||
|
||||
const ETIQUETAS = {
|
||||
ia: { nombre: 'Inteligencia artificial', icono: '🧠', color: '#8eb02f' },
|
||||
ocr: { nombre: 'Lectura de imágenes', icono: '🖼️', color: '#2f7fb0' },
|
||||
whisper: { nombre: 'Transcripción de audio', icono: '🎙️', color: '#b0752f' },
|
||||
ia: { nombre: 'Inteligencia artificial', icono: 'cerebro', color: '#8eb02f' },
|
||||
ocr: { nombre: 'Lectura de imágenes', icono: 'imagen', color: '#2f7fb0' },
|
||||
whisper: { nombre: 'Transcripción de audio', icono: 'microfono', color: '#b0752f' },
|
||||
}
|
||||
const etiqueta = (t) => ETIQUETAS[t] || { nombre: t, icono: '•', color: '#94a3b8' }
|
||||
const etiqueta = (t) => ETIQUETAS[t] || { nombre: t, icono: 'grafico', color: '#94a3b8' }
|
||||
|
||||
async function cargar() {
|
||||
cargando.value = true
|
||||
@@ -88,7 +89,7 @@ watch(() => props.id, cargar, { immediate: true })
|
||||
<h1 class="text-xl font-semibold text-texto">Consumo</h1>
|
||||
<p class="text-sm text-tenue mt-0.5">Lo que usaron tus agentes y cuánto cuesta.</p>
|
||||
</div>
|
||||
<router-link :to="`/tenants/${id}`" class="btn-ghost">← Agentes</router-link>
|
||||
<router-link :to="`/tenants/${id}`" class="btn-ghost inline-flex items-center gap-1.5"><UiIcono nombre="atras" :tam="15" /> Agentes</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Filtros -->
|
||||
@@ -162,12 +163,12 @@ watch(() => props.id, cargar, { immediate: true })
|
||||
</div>
|
||||
<UiEmptyState
|
||||
v-if="resumen.length === 0"
|
||||
icono="📊"
|
||||
estado="durmiendo"
|
||||
titulo="Sin consumo en este período"
|
||||
detalle="Cuando tus agentes respondan mensajes, lean imágenes o transcriban audios, vas a verlo acá."
|
||||
/>
|
||||
<div v-for="r in resumen" :key="r.tipo" class="px-4 py-3 flex items-center gap-3">
|
||||
<span class="text-lg">{{ etiqueta(r.tipo).icono }}</span>
|
||||
<UiIcono :nombre="etiqueta(r.tipo).icono" :tam="18" class="text-tenue" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm text-texto">{{ etiqueta(r.tipo).nombre }}</p>
|
||||
<p class="text-xs text-tenue tabular-nums">
|
||||
|
||||
Reference in New Issue
Block a user