avances poderosos en ui
Some checks failed
build-and-deploy / filter (push) Successful in 4s
build-and-deploy / build (push) Failing after 19s
build-and-deploy / deploy (push) Has been skipped

This commit is contained in:
2025-05-25 08:21:57 -06:00
parent 55645d0cdd
commit 421ff236ae
40 changed files with 1669 additions and 425 deletions

View File

@@ -1,30 +1,24 @@
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TopBar from '@/components/ui/TopBar.vue'
import NavBar from '@/components/ui/NavBar.vue'
import { useUi } from '@/stores/useUi'
const ui = useUi()
</script>
<template>
<div>
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
<!-- TopBar fija -->
<TopBar />
<!-- wrapper: deja espacio para TopBar (pt-14 = 56px) y, en desktop, para NavBar (pl-60) -->
<div :class="['pt-14 min-h-screen bg-gray-100 text-gray-900 transition-[padding-left] duration-200', ui.sidebarOpen ? 'md:pl-60' : '']">
<!-- NavBar fija -->
<NavBar />
<!-- contenido principal -->
<main class="min-h-[calc(100vh-56px)] flex flex-col overflow-hidden">
<RouterView class="flex-1 overflow-auto" />
</main>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
<style scoped></style>

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,83 @@
<script setup>
import { ref, nextTick, onMounted, watch } from 'vue'
import { useChat } from '@/stores/useChat'
const chat = useChat()
const msg = ref('')
const list = ref(null)
function scrollBottom () {
nextTick(() => list.value?.scrollTo({ top: list.value.scrollHeight, behavior: 'smooth' }))
}
function send () {
const t = msg.value.trim()
if (!t) return
if (t.startsWith('/')) chat.run(t.slice(1))
else chat.add({ type: 'text', owner: 'yo', text: t })
msg.value = ''
scrollBottom()
}
function handleKey (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
send()
}
}
onMounted(() => {
if (!chat.items.length) {
chat.add({ type: 'text', owner: 'bot', text: '¡Hola! Probá /empleados, /tareas, etc.' })
}
scrollBottom()
})
watch(() => chat.items.length, scrollBottom)
</script>
<template>
<!-- se adapta al contenedor flex, sin superponer la sidebar -->
<div class="flex flex-col flex-1 min-h-0 bg-gray-50">
<!-- historial -->
<div ref="list" class="flex-1 min-h-0 overflow-auto p-6 space-y-4 custom-scroll">
<template v-for="(m,i) in chat.items" :key="i">
<!-- mensaje de texto -->
<div :class="m.owner==='yo' ? 'flex justify-end' : 'flex justify-start'" v-if="m.type==='text'">
<div
class="max-w-lg rounded-lg px-4 py-2 shadow break-words"
:class="m.owner==='yo' ? 'bg-teal-600 text-white' : 'bg-white text-gray-900'">
{{ m.text }}
</div>
</div>
<!-- componente dinámico -->
<component v-else :is="m.is" v-bind="m.props" />
</template>
</div>
<!-- input -->
<form @submit.prevent="send" class="border-t bg-white p-4 flex gap-2">
<textarea
v-model="msg"
@keydown="handleKey"
rows="1"
placeholder="Escribí un mensaje… (Enter para enviar, Shift+Enter salto)"
class="flex-1 resize-none rounded-lg border p-3 focus:outline-none focus:ring-2 focus:ring-teal-500 custom-scroll"
/>
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition">
</button>
</form>
</div>
</template>
<style scoped>
.custom-scroll::-webkit-scrollbar { width: 8px; }
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
.custom-scroll::-webkit-scrollbar-thumb { background-color: rgba(13,148,136,.35); border-radius: 4px; }
.custom-scroll:hover::-webkit-scrollbar-thumb { background-color: rgba(13,148,136,.7); }
.custom-scroll { scrollbar-width: thin; scrollbar-color: rgba(13,148,136,.6) transparent; }
</style>

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,71 @@
<script setup>
import { ref, watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import { useUi } from '@/stores/useUi'
const ui = useUi()
// enlaces de la app
const links = [
{ to: '/', label: 'Chat', icon: '💬' },
{ to: '/empleados', label: 'Empleados', icon: '👥' },
{ to: '/tareas', label: 'Tareas', icon: '📋' },
{ to: '/planillas', label: 'Planillas', icon: '📂' },
{ to: '/asistencias', label: 'Asistencias', icon: '⏰' },
{ to: '/config', label: 'Config', icon: '⚙️' },
]
const route = useRoute()
const activePath = ref(route.path)
watch(route, v => (activePath.value = v.path))
// clases dinámicas p/ mostrar / ocultar barra
const sidebarClasses = computed(() => ui.sidebarOpen ? 'translate-x-0' : '-translate-x-full')
</script>
<template>
<!-- backdrop en mobile -->
<div v-if="ui.sidebarOpen" class="fixed inset-0 bg-black/40 md:hidden" @click="ui.closeSidebar" />
<!-- barra lateral -->
<aside
:class="['fixed left-0 top-0 md:top-14 h-screen w-60 bg-white dark:bg-zinc-900 border-r border-gray-200 dark:border-zinc-800 flex flex-col select-none z-50 transform transition-transform duration-200 ease-in-out', sidebarClasses]">
<!-- encabezado dentro de sidebar -->
<div class="flex items-center justify-between px-4 py-4 md:px-5 md:py-4 border-b border-gray-200 dark:border-zinc-800 md:border-none">
<span class="text-lg font-semibold text-teal-600 dark:text-teal-400 md:hidden">Núcleo</span>
<button class="h-8 w-8 inline-flex items-center justify-center text-gray-500 hover:text-teal-600" @click="ui.toggleSidebar">
</button>
</div>
<!-- navegación -->
<nav class="flex-1 overflow-y-auto custom-scroll pr-1 pt-4 md:pt-0">
<ul class="space-y-1 px-2">
<li v-for="l in links" :key="l.to">
<RouterLink
:to="l.to"
class="flex items-center gap-3 w-full px-3 py-2 rounded-md font-medium transition group"
:class="activePath.startsWith(l.to)
? 'bg-teal-600 text-white shadow'
: 'text-gray-700 dark:text-gray-100 hover:bg-teal-100 hover:text-teal-900 dark:hover:bg-zinc-800'"
@click="ui.closeSidebar()"
>
<span class="text-lg" aria-hidden="true">{{ l.icon }}</span>
<span class="truncate">{{ l.label }}</span>
</RouterLink>
</li>
</ul>
</nav>
</aside>
</template>
<style scoped>
ul { list-style: none; padding-left: 0; }
.custom-scroll::-webkit-scrollbar { width: 8px; }
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
.custom-scroll::-webkit-scrollbar-thumb { background-color: rgba(13,148,136,.4); border-radius: 4px; }
.custom-scroll:hover::-webkit-scrollbar-thumb { background-color: rgba(13,148,136,.7); }
.custom-scroll { scrollbar-width: thin; scrollbar-color: rgba(13,148,136,.6) transparent; }
</style>

View File

View File

@@ -0,0 +1,23 @@
<script setup>
import { useUi } from '@/stores/useUi'
const ui = useUi()
</script>
<template>
<!-- barra superior fija -->
<header class="fixed top-0 left-0 right-0 h-14 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between px-4 md:px-6 z-50 shadow-sm">
<!-- título -->
<h1 class="text-lg font-semibold tracking-wide text-teal-600 dark:text-teal-400 select-none">Núcleo</h1>
<!-- botón hamburguesa (visible solo en mobile) -->
<button
@click="ui.toggleSidebar"
class="inline-flex items-center justify-center h-9 w-9 rounded-md bg-teal-600 text-white hover:bg-teal-700 transition ">
&#9776;
</button>
</header>
</template>
<style scoped>
/* sin estilos extra */
</style>

View File

@@ -1,5 +1,14 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router'
createApp(App).mount('#app')
import App from './App.vue'
import './style.css' // Tailwind o tus estilos globales
const app =
createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

35
ui/src/router/index.js Normal file
View File

@@ -0,0 +1,35 @@
import { createRouter, createMemoryHistory } from 'vue-router'
const routes = [
// Chat principal y config
{ path: '/', name: 'chat', component: () => import('@/views/ChatView.vue') },
{ path: '/config', name: 'settings', component: () => import('@/views/SettingsView.vue') },
// ────── Empleados ──────
{ path: '/empleados', name: 'empleados-index', component: () => import('@/views/empleados/EmpleadosIndex.vue') },
{ path: '/empleados/nuevo', name: 'empleados-new', component: () => import('@/views/empleados/EmpleadoForm.vue') },
{ path: '/empleados/:id', name: 'empleados-edit', component: () => import('@/views/empleados/EmpleadoForm.vue'), props: true },
// ────── Tareas ──────
{ path: '/tareas', name: 'tareas-index', component: () => import('@/views/tareas/TareasIndex.vue') },
{ path: '/tareas/nuevo', name: 'tareas-new', component: () => import('@/views/tareas/TareaForm.vue') },
{ path: '/tareas/:id', name: 'tareas-edit', component: () => import('@/views/tareas/TareaForm.vue'), props: true },
// ────── Planillas ──────
{ path: '/planillas', name: 'planillas-index', component: () => import('@/views/planillas/PlanillasIndex.vue') },
{ path: '/planillas/nuevo', name: 'planillas-new', component: () => import('@/views/planillas/PlanillaForm.vue') },
{ path: '/planillas/:id', name: 'planillas-edit', component: () => import('@/views/planillas/PlanillaForm.vue'), props: true },
// ────── Asistencias ──────
{ path: '/asistencias', name: 'asistencias-index', component: () => import('@/views/asistencias/AsistenciasIndex.vue') },
{ path: '/asistencias/nuevo', name: 'asistencias-new', component: () => import('@/views/asistencias/AsistenciaForm.vue') },
{ path: '/asistencias/:id', name: 'asistencias-edit', component: () => import('@/views/asistencias/AsistenciaForm.vue'), props: true },
// 404
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/NotFound.vue') }
]
export default createRouter({
history: createMemoryHistory(),
routes,
})

View File

@@ -0,0 +1,5 @@
import { defineStore } from 'pinia'
export const useAsistencias = defineStore('asistencias', {
state: () => ({ asistencias: [] }),
})

42
ui/src/stores/useChat.js Normal file
View File

@@ -0,0 +1,42 @@
import { defineStore } from 'pinia'
/**
* Chat store
* - items: hist de mensajes [{ type, owner, text, … }]
* - add(): agrega un mensaje
* - run(): ejecuta /comandos y mete componentes dinámicos
*/
export const useChat = defineStore('chat', {
state: () => ({
items: [],
}),
actions: {
add (item) {
this.items.push(item)
},
async run (cmd) {
switch (cmd) {
case 'empleados':
// para demo solo mostramos un texto, después inyectaremos un componente
this.add({ type: 'text', owner: 'bot', text: '🎉 Módulo Empleados aún en construcción.' })
break
case 'tareas':
this.add({ type: 'text', owner: 'bot', text: '🛠️ Módulo Tareas aún en construcción.' })
break
case 'planillas':
this.add({ type: 'text', owner: 'bot', text: '📂 Módulo Planillas en construcción.' })
break
case 'asistencias':
this.add({ type: 'text', owner: 'bot', text: '⏰ Módulo Asistencias en construcción.' })
break
default:
this.add({ type: 'text', owner: 'bot', text: `❓ No reconozco /${cmd}` })
}
},
},
})

View File

@@ -0,0 +1,12 @@
import { defineStore } from 'pinia'
export const useEmpleados = defineStore('empleados', {
state: () => ({ empleados: [] }),
actions: {
// placeholder para cargar/crear empleados
async fetchAll () {
// simulamos fetch
this.empleados = []
},
},
})

View File

@@ -0,0 +1,5 @@
import { defineStore } from 'pinia'
export const usePlanillas = defineStore('planillas', {
state: () => ({ planillas: [] }),
})

View File

@@ -0,0 +1,5 @@
import { defineStore } from 'pinia'
export const useTareas = defineStore('tareas', {
state: () => ({ tareas: [] }),
})

20
ui/src/stores/useUI.js Normal file
View File

@@ -0,0 +1,20 @@
// src/stores/useUi.js
import { defineStore } from 'pinia'
export const useUi = defineStore('ui', {
state: () => ({
sidebarOpen: true, // visible por defecto en desktop
}),
actions: {
toggleSidebar () {
this.sidebarOpen = !this.sidebarOpen
},
closeSidebar () {
this.sidebarOpen = false
},
openSidebar () {
this.sidebarOpen = true
},
},
})

View File

@@ -1,79 +1,4 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;

14
ui/src/views/ChatView.vue Normal file
View File

@@ -0,0 +1,14 @@
<script setup>
/* Vista raíz “/” → muestra el chat estilo ChatGPT */
import CanvasChat from '@/components/chat/CanvasChat.vue'
</script>
<template>
<div class="h-full flex flex-col">
<CanvasChat class="flex-1" />
</div>
</template>
<style scoped>
/* nada por ahora */
</style>

21
ui/src/views/NotFound.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div class="h-full flex flex-col items-center justify-center gap-6 p-8 text-center">
<h1 class="text-5xl font-bold text-teal-600">404</h1>
<p class="text-lg text-gray-700">Uy, no encontramos esa página.</p>
<button
@click="router.push('/')"
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition">
Volver al inicio
</button>
</div>
</template>
<style scoped>
/* Estilos extra opcionales */
</style>

View File

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@