leonardo 1 hafta önce
ebeveyn
işleme
4d51f3289e

+ 5 - 4
src/layout/HeaderSistema.vue

@@ -55,17 +55,18 @@ async function encerrarSessao() {
 
 <template>
   <header
-    class="col-span-full row-start-1 relative z-30 grid grid-cols-[var(--layout-sidebar-width)_1fr] items-center gap-[18px] border-b border-gray-200 bg-primary px-[18px] py-[14px] text-primary-foreground dark:border-gray-700 dark:bg-primary max-layout:flex"
+    class="col-span-full row-start-1 relative z-30 grid grid-cols-[var(--layout-sidebar-width)_1fr] items-center gap-4 border-b border-border bg-primary px-5 py-3.5 text-primary-foreground dark:bg-primary max-layout:flex transition-grid-cols"
   >
     <div class="min-w-0 overflow-hidden">
       <div
-        class="truncate whitespace-nowrap text-[16px] font-extrabold tracking-[0.14em]"
+        class="truncate whitespace-nowrap text-[16px] font-extrabold"
+        :class="sidebarRecolhida ? 'tracking-wide' : 'tracking-[0.14em]'"
       >
         ORÁCULO
       </div>
       <div
-        v-if="!sidebarRecolhida"
-        class="mt-1.5 text-xs text-primary-foreground/80"
+        class="mt-1.5 truncate whitespace-nowrap text-xs text-primary-foreground/80"
+        :class="sidebarRecolhida ? 'invisible' : ''"
       >
         Assistente interno com base de conhecimento
       </div>

+ 1 - 1
src/layout/LayoutSistema.vue

@@ -10,7 +10,7 @@ const { layoutStyle } = useSidebar();
 <template>
   <div class="h-screen overflow-hidden bg-background text-foreground">
     <div
-      class="[--layout-header-height:5rem] relative grid h-full min-h-0 grid-cols-[var(--layout-sidebar-width)_1fr] grid-rows-[var(--layout-header-height)_1fr] overflow-hidden transition-[grid-template-columns] duration-200 ease-in-out max-layout:grid-cols-1 max-layout:grid-rows-[var(--layout-header-height)_1fr_auto]"
+      class="[--layout-header-height:5rem] relative grid h-full min-h-0 grid-cols-[var(--layout-sidebar-width)_1fr] grid-rows-[var(--layout-header-height)_1fr] overflow-hidden transition-grid-cols max-layout:grid-cols-1 max-layout:grid-rows-[var(--layout-header-height)_1fr_auto]"
       :style="layoutStyle"
     >
       <DecorativeGradient />

+ 127 - 131
src/layout/SidebarSistema.vue

@@ -44,172 +44,168 @@ async function novaConversa() {
 
 <template>
   <aside
-    class="col-start-1 row-start-2 relative z-10 flex min-h-0 min-w-0 flex-col gap-3 overflow-hidden border-r border-gray-200 bg-transparent p-2 dark:border-gray-700 max-layout:row-start-3 max-layout:border-r-0 max-layout:border-t"
+    class="col-start-1 row-start-2 relative z-10 flex min-h-0 min-w-0 flex-col overflow-hidden rounded-[28px] border border-gray-200 bg-white/70 p-3 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-abyss-900 dark:shadow-[0_18px_50px_rgba(0,0,0,0.35)] max-layout:row-start-3 max-layout:max-h-[45vh] max-layout:overflow-y-auto"
   >
     <div
-      class="flex min-h-0 flex-1 flex-col overflow-hidden rounded-[28px] border border-gray-200 bg-white/70 p-3 shadow-sm backdrop-blur-md dark:border-white/10 dark:bg-abyss-900 dark:shadow-[0_18px_50px_rgba(0,0,0,0.35)]"
+      :class="
+        sidebarRecolhida
+          ? 'flex items-center justify-center px-1 pb-1'
+          : 'flex items-center justify-end px-1 pb-1'
+      "
     >
-      <div
-        :class="
-          sidebarRecolhida
-            ? 'flex items-center justify-center px-1 pb-1'
-            : 'flex items-center justify-end px-1 pb-1'
+      <button
+        type="button"
+        class="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-gray-200 bg-white/60 text-gray-600 transition-colors hover:bg-white/80 hover:text-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background dark:border-white/10 dark:bg-white/5 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
+        :aria-label="
+          sidebarRecolhida ? 'Expandir sidebar' : 'Recolher sidebar'
         "
+        :title="sidebarRecolhida ? 'Expandir sidebar' : 'Recolher sidebar'"
+        @click="sidebarRecolhida = !sidebarRecolhida"
+      >
+        <BaseIcon :icon="sidebarRecolhida ? ChevronRight : ChevronLeft" class="h-5 w-5" />
+      </button>
+    </div>
+
+    <div
+      v-if="!sidebarRecolhida"
+      class="px-2 pt-1 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
+    >
+      Chat
+    </div>
+    <nav class="grid gap-1">
+      <button
+        type="button"
+        :class="classeItemSidebar(rotaAtiva === 'home' && !activeConversationId)"
+        :title="sidebarRecolhida ? 'Nova conversa' : ''"
+        @click="novaConversa"
+      >
+        <BaseIcon :icon="Plus" :class="classeIcon(rotaAtiva === 'home' && !activeConversationId)" />
+        <span :class="classeRotulo">Nova conversa</span>
+      </button>
+
+      <button
+        type="button"
+        :class="classeItemSidebar(rotaAtiva === 'conversas-historico')"
+        :title="sidebarRecolhida ? 'Histórico' : ''"
+        @click="irPara('conversas-historico')"
+      >
+        <BaseIcon :icon="History" :class="classeIcon(rotaAtiva === 'conversas-historico')" />
+        <span :class="classeRotulo">Histórico</span>
+      </button>
+
+      <button
+        type="button"
+        :class="classeItemSidebar(rotaAtiva === 'conversas-pesquisar')"
+        :title="sidebarRecolhida ? 'Pesquisar' : ''"
+        @click="irPara('conversas-pesquisar')"
       >
+        <BaseIcon :icon="Search" :class="classeIcon(rotaAtiva === 'conversas-pesquisar')" />
+        <span :class="classeRotulo">Pesquisar</span>
+      </button>
+
+      <button
+        type="button"
+        :class="classeItemSidebar(rotaAtiva === 'conversas-whatsapp')"
+        :title="sidebarRecolhida ? 'Atendimento WhatsApp' : ''"
+        @click="irPara('conversas-whatsapp')"
+      >
+        <BaseIcon :icon="MessageCircle" :class="classeIcon(rotaAtiva === 'conversas-whatsapp')" />
+        <span :class="classeRotulo">Atendimento WhatsApp</span>
+      </button>
+    </nav>
+
+    
+    <ListaConversas v-if="!sidebarRecolhida" />
+
+    
+    <div class="mt-auto flex-shrink-0 grid gap-1 pt-2">
+      <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
+      <template v-if="isAdmin">
+        <div
+          v-if="!sidebarRecolhida"
+          class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
+        >
+          Biblioteca
+        </div>
+
         <button
           type="button"
-          class="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-gray-200 bg-white/60 text-gray-600 transition-colors hover:bg-white/80 hover:text-gray-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background dark:border-white/10 dark:bg-white/5 dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
-          :aria-label="
-            sidebarRecolhida ? 'Expandir sidebar' : 'Recolher sidebar'
-          "
-          :title="sidebarRecolhida ? 'Expandir sidebar' : 'Recolher sidebar'"
-          @click="sidebarRecolhida = !sidebarRecolhida"
+          :class="classeItemSidebar(rotaAtiva === 'documentos-carregar')"
+          :title="sidebarRecolhida ? 'Carregar documento' : ''"
+          @click="irPara('documentos-carregar')"
         >
-          <BaseIcon :icon="sidebarRecolhida ? ChevronRight : ChevronLeft" class="h-5 w-5" />
+          <BaseIcon :icon="Upload" :class="classeIcon(rotaAtiva === 'documentos-carregar')" />
+          <span :class="classeRotulo">Carregar documento</span>
         </button>
-      </div>
 
-      <div
-        v-if="!sidebarRecolhida"
-        class="px-2 pt-1 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
-      >
-        Chat
-      </div>
-      <nav class="grid gap-1">
         <button
           type="button"
-          :class="classeItemSidebar(rotaAtiva === 'home' && !activeConversationId)"
-          :title="sidebarRecolhida ? 'Nova conversa' : ''"
-          @click="novaConversa"
+          :class="classeItemSidebar(rotaAtiva === 'documentos')"
+          :title="sidebarRecolhida ? 'Documentos' : ''"
+          @click="irPara('documentos')"
         >
-          <BaseIcon :icon="Plus" :class="classeIcon(rotaAtiva === 'home' && !activeConversationId)" />
-          <span :class="classeRotulo">Nova conversa</span>
+          <BaseIcon :icon="FileText" :class="classeIcon(rotaAtiva === 'documentos')" />
+          <span :class="classeRotulo">Documentos</span>
         </button>
 
         <button
           type="button"
-          :class="classeItemSidebar(rotaAtiva === 'conversas-historico')"
-          :title="sidebarRecolhida ? 'Histórico' : ''"
-          @click="irPara('conversas-historico')"
+          :class="classeItemSidebar(rotaAtiva === 'avaliacoes')"
+          :title="sidebarRecolhida ? 'Avaliações' : ''"
+          @click="irPara('avaliacoes')"
         >
-          <BaseIcon :icon="History" :class="classeIcon(rotaAtiva === 'conversas-historico')" />
-          <span :class="classeRotulo">Histórico</span>
+          <BaseIcon :icon="Star" :class="classeIcon(rotaAtiva === 'avaliacoes')" />
+          <span :class="classeRotulo">Avaliações</span>
         </button>
 
         <button
           type="button"
-          :class="classeItemSidebar(rotaAtiva === 'conversas-pesquisar')"
-          :title="sidebarRecolhida ? 'Pesquisar' : ''"
-          @click="irPara('conversas-pesquisar')"
+          :class="classeItemSidebar(rotaAtiva === 'ranking-atendentes')"
+          :title="sidebarRecolhida ? 'Ranking de atendentes' : ''"
+          @click="irPara('ranking-atendentes')"
         >
-          <BaseIcon :icon="Search" :class="classeIcon(rotaAtiva === 'conversas-pesquisar')" />
-          <span :class="classeRotulo">Pesquisar</span>
+          <BaseIcon :icon="Trophy" :class="classeIcon(rotaAtiva === 'ranking-atendentes')" />
+          <span :class="classeRotulo">Ranking de atendentes</span>
         </button>
 
         <button
           type="button"
-          :class="classeItemSidebar(rotaAtiva === 'conversas-whatsapp')"
-          :title="sidebarRecolhida ? 'Atendimento WhatsApp' : ''"
-          @click="irPara('conversas-whatsapp')"
+          :class="classeItemSidebar(rotaAtiva === 'atendimentos-nao-resolvidos')"
+          :title="sidebarRecolhida ? 'Não resolvidos' : ''"
+          @click="irPara('atendimentos-nao-resolvidos')"
         >
-          <BaseIcon :icon="MessageCircle" :class="classeIcon(rotaAtiva === 'conversas-whatsapp')" />
-          <span :class="classeRotulo">Atendimento WhatsApp</span>
+          <BaseIcon :icon="TriangleAlert" :class="classeIcon(rotaAtiva === 'atendimentos-nao-resolvidos')" />
+          <span :class="classeRotulo">Não resolvidos</span>
         </button>
-      </nav>
-
-      
-      <ListaConversas v-if="!sidebarRecolhida" />
-
-      
-      <div class="mt-auto flex-shrink-0 grid gap-1 pt-2">
-        <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
-        <template v-if="isAdmin">
-          <div
-            v-if="!sidebarRecolhida"
-            class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
-          >
-            Biblioteca
-          </div>
-
-          <button
-            type="button"
-            :class="classeItemSidebar(rotaAtiva === 'documentos-carregar')"
-            :title="sidebarRecolhida ? 'Carregar documento' : ''"
-            @click="irPara('documentos-carregar')"
-          >
-            <BaseIcon :icon="Upload" :class="classeIcon(rotaAtiva === 'documentos-carregar')" />
-            <span :class="classeRotulo">Carregar documento</span>
-          </button>
-
-          <button
-            type="button"
-            :class="classeItemSidebar(rotaAtiva === 'documentos')"
-            :title="sidebarRecolhida ? 'Documentos' : ''"
-            @click="irPara('documentos')"
-          >
-            <BaseIcon :icon="FileText" :class="classeIcon(rotaAtiva === 'documentos')" />
-            <span :class="classeRotulo">Documentos</span>
-          </button>
-
-          <button
-            type="button"
-            :class="classeItemSidebar(rotaAtiva === 'avaliacoes')"
-            :title="sidebarRecolhida ? 'Avaliações' : ''"
-            @click="irPara('avaliacoes')"
-          >
-            <BaseIcon :icon="Star" :class="classeIcon(rotaAtiva === 'avaliacoes')" />
-            <span :class="classeRotulo">Avaliações</span>
-          </button>
-
-          <button
-            type="button"
-            :class="classeItemSidebar(rotaAtiva === 'ranking-atendentes')"
-            :title="sidebarRecolhida ? 'Ranking de atendentes' : ''"
-            @click="irPara('ranking-atendentes')"
-          >
-            <BaseIcon :icon="Trophy" :class="classeIcon(rotaAtiva === 'ranking-atendentes')" />
-            <span :class="classeRotulo">Ranking de atendentes</span>
-          </button>
-
-          <button
-            type="button"
-            :class="classeItemSidebar(rotaAtiva === 'atendimentos-nao-resolvidos')"
-            :title="sidebarRecolhida ? 'Não resolvidos' : ''"
-            @click="irPara('atendimentos-nao-resolvidos')"
-          >
-            <BaseIcon :icon="TriangleAlert" :class="classeIcon(rotaAtiva === 'atendimentos-nao-resolvidos')" />
-            <span :class="classeRotulo">Não resolvidos</span>
-          </button>
-
-          <button
-            type="button"
-            :class="classeItemSidebar(rotaAtiva === 'golden-set')"
-            :title="sidebarRecolhida ? 'Golden set' : ''"
-            @click="irPara('golden-set')"
-          >
-            <BaseIcon :icon="CircleCheck" :class="classeIcon(rotaAtiva === 'golden-set')" />
-            <span :class="classeRotulo">Golden set</span>
-          </button>
-
-          <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
-        </template>
-        <div
-          v-if="!sidebarRecolhida"
-          class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
-        >
-          Sistema
-        </div>
 
         <button
           type="button"
-          :class="classeItemSidebar(rotaAtiva === 'configuracoes')"
-          :title="sidebarRecolhida ? 'Configurações' : ''"
-          @click="irPara('configuracoes')"
+          :class="classeItemSidebar(rotaAtiva === 'golden-set')"
+          :title="sidebarRecolhida ? 'Golden set' : ''"
+          @click="irPara('golden-set')"
         >
-          <BaseIcon :icon="SlidersHorizontal" :class="classeIcon(rotaAtiva === 'configuracoes')" />
-          <span :class="classeRotulo">Configurações</span>
+          <BaseIcon :icon="CircleCheck" :class="classeIcon(rotaAtiva === 'golden-set')" />
+          <span :class="classeRotulo">Golden set</span>
         </button>
+
+        <div class="my-1 h-px bg-black/10 dark:bg-white/10" />
+      </template>
+      <div
+        v-if="!sidebarRecolhida"
+        class="px-2 pb-0.5 text-[10px] font-semibold uppercase tracking-widest text-gray-400 dark:text-white/30 select-none"
+      >
+        Sistema
       </div>
+
+      <button
+        type="button"
+        :class="classeItemSidebar(rotaAtiva === 'configuracoes')"
+        :title="sidebarRecolhida ? 'Configurações' : ''"
+        @click="irPara('configuracoes')"
+      >
+        <BaseIcon :icon="SlidersHorizontal" :class="classeIcon(rotaAtiva === 'configuracoes')" />
+        <span :class="classeRotulo">Configurações</span>
+      </button>
     </div>
   </aside>
 </template>

+ 7 - 0
src/styles/tailwind.css

@@ -373,6 +373,13 @@ body {
 }
 
 @layer utilities {
+  /* transição de grid-template-columns ao recolher/expandir a sidebar;
+     compartilhada entre o grid raiz (LayoutSistema) e o grid interno do
+     header (HeaderSistema), que replica --layout-sidebar-width */
+  .transition-grid-cols {
+    transition: grid-template-columns 200ms ease-in-out;
+  }
+
   /* scrollbar fina usada em listas/painéis com overflow (sidebar, whatsapp, chat) */
   .scroll-thin {
     scrollbar-width: thin;

+ 0 - 1
src/utils/data.js

@@ -54,7 +54,6 @@ export function ehNovoDia(atual, anterior) {
   return new Date(atual).toDateString() !== new Date(anterior).toDateString();
 }
 
-// rótulo curto de recência para listas de conversa: hora se for hoje, "Ontem", senão dd/mm
 export function formatarRecencia(valor) {
   const d = paraData(valor);
   if (!d) return "";