Repository: AppCivico/smae
Pull request #263: Feature/SmaeTooltip
Author: login: GustavoFSoares
Created: 2025-04-29T17:33:28Z | Merged: 2025-04-30T16:46:53Z

[Task ID](https://trello.com/c/Ff3N3Tn6/2134-sp6gp004-incluir-icone-de-informacao-em-campos)


Diff:
diff --git a/frontend/src/components/SmaeTooltip/SmaeTooltip.vue b/frontend/src/components/SmaeTooltip/SmaeTooltip.vue
new file mode 100644
index 0000000000..fce7015539
--- /dev/null
+++ b/frontend/src/components/SmaeTooltip/SmaeTooltip.vue
@@ -0,0 +1,130 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+
+type Slots = {
+  default(): void
+  botao(): void
+};
+
+type Props = {
+  texto?: string
+  icone?: string
+};
+
+defineSlots<Slots>();
+withDefaults(defineProps<Props>(), {
+  icone: 'i',
+  texto: undefined,
+});
+
+const content = ref<HTMLElement>();
+const exibicaoTooltip = ref<boolean>(false);
+const manterExibido = ref<boolean>(false);
+
+const statusTooltip = computed<boolean>(() => {
+  if (manterExibido.value) {
+    return false;
+  }
+
+  return !exibicaoTooltip.value;
+});
+
+const descricaoConteudo = computed<string>(() => {
+  if (!content.value) {
+    return '';
+  }
+
+  return content.value.textContent;
+});
+
+function esconderTooltip() {
+  exibicaoTooltip.value = false;
+}
+
+function exibirTooltip() {
+  exibicaoTooltip.value = true;
+}
+
+function trocarManterAberto() {
+  manterExibido.value = !manterExibido.value;
+}
+</script>
+
+<template>
+  <div
+    :aria-described="descricaoConteudo"
+    :class="['smae-tooltip', { 'smae-tooltip--fixado': manterExibido }]"
+    tabindex="0"
+    @mouseenter="exibirTooltip"
+    @focus="exibirTooltip"
+    @mouseleave="esconderTooltip"
+    @blur="esconderTooltip"
+    @click="trocarManterAberto"
+  >
+    <slot name="botao">
+      <svg
+        width="20"
+        height="20"
+      ><use :xlink:href="`#i_${$props.icone}`" /></svg>
+    </slot>
+
+    <div
+      ref="content"
+      class="smae-tooltip__content"
+      role="tooltip"
+      :hidden="statusTooltip"
+    >
+      <slot>{{ $props.texto }}</slot>
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.smae-tooltip {
+  display: inline-block;
+  vertical-align: middle;
+  color: @marrom;
+  background-color: transparent;
+  border: none;
+  padding: 0;
+  cursor: pointer;
+
+  > svg {
+    display: inline-block;
+  }
+}
+
+.smae-tooltip--fixado {
+  color: #22222a
+}
+
+.smae-tooltip__content {
+  width: max-content;
+  max-width: 200px;
+  padding: 1rem;
+  color: white;
+  background-color: @primary;
+  border-radius: .5rem;
+  line-height: 1.4;
+  text-align: center;
+  text-transform: none;
+  white-space: normal;
+  pointer-events: none;
+  position: fixed;
+  z-index: 999;
+  animation: fadeIn .5s;
+  transform: translate(calc(-50% + 10px), calc(-100% - 24px - 0.5rem));
+
+  &:before {
+    content: "";
+    position: absolute;
+    left: calc(50% - 0.5rem);
+    transform: rotate(-45deg);
+    bottom: 1px;
+    margin: 0 0 -0.5rem 0;
+    border: .5rem solid transparent;
+    border-bottom-color: @primary;
+    border-left-color: @primary;
+  }
+}
+</style>
diff --git a/frontend/src/components/camposDeFormulario/LabelFromYup.vue b/frontend/src/components/camposDeFormulario/SmaeLabel.vue
similarity index 56%
rename from frontend/src/components/camposDeFormulario/LabelFromYup.vue
rename to frontend/src/components/camposDeFormulario/SmaeLabel.vue
index 244b03a1a2..b535f2be4e 100644
--- a/frontend/src/components/camposDeFormulario/LabelFromYup.vue
+++ b/frontend/src/components/camposDeFormulario/SmaeLabel.vue
@@ -1,6 +1,14 @@
-<script setup>
+<script setup lang="ts">
 import { computed } from 'vue';
 import buscarDadosDoYup from './helpers/buscarDadosDoYup';
+import SmaeTooltip from '../SmaeTooltip/SmaeTooltip.vue';
+
+type Slots = {
+  default(props: { label: string }): void
+  prepend(): void
+  append(): void
+  balaoInformativo: unknown
+};
 
 const props = defineProps({
   as: {
@@ -26,30 +34,71 @@ const props = defineProps({
     type: Boolean,
     default: true,
   },
+  balaoInformativo: {
+    type: String,
+    default: undefined,
+  },
 });
 
+const slots = defineSlots<Slots>();
+
 const caminhoNoSchema = computed(() => buscarDadosDoYup(props.schema, props.name));
+
+const temInformacao = computed<boolean>(() => {
+  if (props.balaoInformativo) {
+    return true;
+  }
+
+  if (caminhoNoSchema.value.spec.meta?.balaoInformativo) {
+    return true;
+  }
+
+  if (slots.balaoInformativo) {
+    return true;
+  }
+
+  return false;
+});
 </script>
+
 <template>
   <component
     :is="as"
+    class="smae-label flex center"
     :class="{ label: classeLabel && as !== 'legend' }"
     :for="as !== 'label'
       ? undefined
       : name || $attrs.for || null"
   >
     <slot name="prepend" />
+
     <slot :label="caminhoNoSchema?.spec?.label || name">
       <pre
         v-if="!caminhoNoSchema"
         v-ScrollLockDebug
       >
-      Etiqueta não encontrada para `{{ name }}`
-    </pre>
+        Etiqueta não encontrada para `{{ name }}`
+      </pre>
+
       <template v-else>
         <template v-if="schema">
           {{ caminhoNoSchema?.spec?.label || `Campo: ${name}` }}
-        </template>&nbsp;<span
+        </template>&nbsp;
+
+        <small v-if="temInformacao">
+          <SmaeTooltip
+            :texto="
+              $props.balaoInformativo
+                || caminhoNoSchema.spec.meta?.balaoInformativo
+            "
+          >
+            <slot name="balaoInformativo" />
+          </SmaeTooltip>
+
+          &nbsp;
+        </small>
+
+        <span
           v-if="required ||
             (
               caminhoNoSchema?.spec?.presence === 'required'
diff --git a/frontend/src/components/notas/NotasLista.vue b/frontend/src/components/notas/NotasLista.vue
index 790957793c..ae3bc3da74 100644
--- a/frontend/src/components/notas/NotasLista.vue
+++ b/frontend/src/components/notas/NotasLista.vue
@@ -1,11 +1,11 @@
 <script setup>
-import removerHtml from '@/helpers/removerHtml';
+import { storeToRefs } from 'pinia';
+import { onMounted, ref, watch } from 'vue';
+import removerHtml from '@/helpers/html/removerHtml';
 import { useAlertStore } from '@/stores/alert.store';
 import { useBlocoDeNotasStore } from '@/stores/blocoNotas.store';
 import { useTipoDeNotasStore } from '@/stores/tipoNotas.store';
 import { useTransferenciasVoluntariasStore } from '@/stores/transferenciasVoluntarias.store';
-import { storeToRefs } from 'pinia';
-import { onMounted, ref, watch } from 'vue';
 import SmaeLink from '../SmaeLink.vue';
 
 const status = {
@@ -163,7 +163,7 @@ watch(statusSelecionado, (novoValor) => {
               height="20"
             ><use xlink:href="#i_edit" /></svg>
           </SmaeLink>
-        </td>        
+        </td>
         <td>
           <button
             v-if="item.pode_editar"
@@ -180,7 +180,6 @@ watch(statusSelecionado, (novoValor) => {
             </svg>
           </button>
         </td>
-
       </tr>
       <tr>
         <td colspan="6">
diff --git a/frontend/src/components/notas/QuadroNotas.vue b/frontend/src/components/notas/QuadroNotas.vue
index b1cad25334..73a8e70221 100644
--- a/frontend/src/components/notas/QuadroNotas.vue
+++ b/frontend/src/components/notas/QuadroNotas.vue
@@ -17,7 +17,7 @@
           }"
           class="tprimary"
         >
-          {{ item.transferencia_identificador}}
+          {{ item.transferencia_identificador }}
         </router-link>
       </p>
       <div class="text">
@@ -59,13 +59,13 @@
 </template>
 
 <script setup>
-import esferasDeTransferencia from '@/consts/esferasDeTransferencia';
-import removerHtml from '@/helpers/removerHtml';
-import truncate from '@/helpers/texto/truncate';
-import { useBlocoDeNotasStore } from '@/stores/blocoNotas.store';
 import { storeToRefs } from 'pinia';
 import { watch } from 'vue';
 import { useRoute } from 'vue-router';
+import esferasDeTransferencia from '@/consts/esferasDeTransferencia';
+import removerHtml from '@/helpers/html/removerHtml';
+import truncate from '@/helpers/texto/truncate';
+import { useBlocoDeNotasStore } from '@/stores/blocoNotas.store';
 
 const blocoStore = useBlocoDeNotasStore();
 const {
@@ -98,12 +98,16 @@ watch([
   () => route.query.palavra_chave,
   () => route.query.atividade,
 ], () => {
-  let {
+  const {
     partido_ids: partidoFiltro,
     orgaos_ids: orgaoFiltro,
-    palavra_chave: palavraChaveParaBusca,
     atividade: atividadeFiltro,
   } = route.query;
+
+  let {
+    palavra_chave: palavraChaveParaBusca,
+  } = route.query;
+
   if (typeof palavraChaveParaBusca === 'string') {
     palavraChaveParaBusca = palavraChaveParaBusca.trim();
   }
diff --git a/frontend/src/consts/formSchemas.js b/frontend/src/consts/formSchemas.js
index e6ab3863b8..4112bc778e 100644
--- a/frontend/src/consts/formSchemas.js
+++ b/frontend/src/consts/formSchemas.js
@@ -1,4 +1,18 @@
 /* eslint-disable no-template-curly-in-string */
+import { isAfter, isBefore } from 'date-fns';
+import {
+  addMethod,
+  array,
+  boolean,
+  date,
+  lazy,
+  mixed,
+  number,
+  object,
+  ref,
+  setLocale,
+  string,
+} from 'yup';
 import cargosDeParlamentar from '@/consts/cargosDeParlamentar';
 import categoriaDeTransferencia from '@/consts/categoriaDeTransferencia';
 import esferasDeTransferencia from '@/consts/esferasDeTransferencia';
@@ -22,20 +36,6 @@ import tiposNaEquipeDeParlamentar from '@/consts/tiposNaEquipeDeParlamentar';
 import tiposSituacaoSchema from '@/consts/tiposSituacaoSchema';
 import fieldToDate from '@/helpers/fieldToDate';
 import haDuplicatasNaLista from '@/helpers/haDuplicatasNaLista';
-import { isAfter, isBefore } from 'date-fns';
-import {
-  addMethod,
-  array,
-  boolean,
-  date,
-  lazy,
-  mixed,
-  number,
-  object,
-  ref,
-  setLocale,
-  string,
-} from 'yup';
 import tiposStatusDistribuicao from './tiposStatusDistribuicao';
 
 const dataMin = import.meta.env.VITE_DATA_MIN ? new Date(`${import.meta.env.VITE_DATA_MIN}`) : new Date('1900-01-01T00:00:00Z');
@@ -2594,13 +2594,16 @@ export const projeto = object()
       .max(dataMax)
       .min(new Date(2003, 0, 1)),
     equipe: array()
-      .label('Equipe')
+      .label('Equipe do Projeto')
       .nullable()
       .of(
         number()
           .label('Pessoa')
           .required(),
-      ),
+      )
+      .meta({
+        balaoInformativo: 'É composta por pessoas responsáveis por executar atividades planejadas e contribuir diretamente para o cumprimento dos objetivos do projeto, conforme escopo definido. Suas principais responsabilidades incluem colaborar com outros membros da equipe, atualizar as informações, reportar ao gerente de projeto e propor soluções para eventuais problemas.',
+      }),
     grupo_portfolio: array()
       .label('Grupos de observadores')
       .nullable()
@@ -2688,14 +2691,23 @@ export const projeto = object()
       .max(2048)
       .nullable(),
     orgao_gestor_id: number()
-      .label('Órgão gestor')
+      .label('Órgão Gestor do Portfólio - Escritório do Projeto')
       .min(1, 'Selecione um órgão gestor')
-      .required('O projeto necessita de um órgão gestor'),
+      .required('O projeto necessita de um órgão gestor')
+      .meta({
+        balaoInformativo: 'É o órgão que cumpre a função de Escritório de Projetos, sendo responsável pelo portfólio no qual o projeto está inserido.',
+      }),
     orgao_responsavel_id: number()
       .label('Órgão responsável')
-      .nullable(),
+      .nullable()
+      .meta({
+        balaoInformativo: 'É o órgão responsável pela execução do projeto.',
+      }),
     orgaos_participantes: array()
-      .label('Órgãos participantes'),
+      .label('Órgãos participantes')
+      .meta({
+        balaoInformativo: 'São os órgãos nos quais estão alocadas as demais partes interessadas do projeto.',
+      }),
     origem_outro: string()
       .label('Descrição de origem fora do PdM')
       .max(2048)
@@ -2778,11 +2790,17 @@ export const projeto = object()
       .label('Região')
       .nullable(),
     responsaveis_no_orgao_gestor: array()
-      .label('Responsável pelo acompanhamento')
-      .nullable(),
+      .label('Assessor do Escritório de Projetos')
+      .nullable()
+      .meta({
+        balaoInformativo: 'É a pessoa que atua como facilitador e apoiador na aplicação das metodologias definidas. Suas principais responsabilidades incluem fornecer suporte técnico e metodológico ao gerente do projeto, durante todas as fases, exercendo poder de influência para o alinhamento do projeto com os objetivos estratégicos definidos pela organização.',
+      }),
     responsavel_id: number()
       .label('Gerente do projeto')
-      .nullable(),
+      .nullable()
+      .meta({
+        balaoInformativo: 'É a pessoa responsável pela evolução do projeto em todas as suas fases. Suas principais funções incluem definir o escopo, elaborar cronogramas, alocar recursos, gerenciar riscos, liderar a equipe e manter uma comunicação eficaz com todas as partes interessadas, garantindo que todos estejam alinhados e que as entregas ocorram conforme o planejado, promovendo ajustes sempre que necessário.',
+      }),
     restricoes: array()
       .label('Restrições')
       .of(
@@ -2800,11 +2818,17 @@ export const projeto = object()
       .label('Resumo')
       .max(2048),
     secretario_executivo: string()
-      .label('Secretário gestor')
-      .nullable(),
+      .label('Secretário Gestor do Portfólio')
+      .nullable()
+      .meta({
+        balaoInformativo: 'É o(a) secretário(a) do Órgão Gestor do Portfólio.',
+      }),
     secretario_responsavel: string()
       .label('Secretário responsável')
-      .nullable(),
+      .nullable()
+      .meta({
+        balaoInformativo: 'É o(a) secretário(a) do Órgão Responsável.',
+      }),
     status: mixed()
       .label('Status')
       .oneOf(Object.keys(statusDeProjetos))
diff --git a/frontend/src/helpers/removerHtml.js b/frontend/src/helpers/html/removerHtml.js
similarity index 100%
rename from frontend/src/helpers/removerHtml.js
rename to frontend/src/helpers/html/removerHtml.js
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index dcec2a2485..82d1de5c3c 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -1,7 +1,13 @@
+import type { Store } from 'pinia';
+import { createPinia } from 'pinia';
+import {
+  createApp, markRaw, nextTick,
+} from 'vue';
+import type { RouteLocationNormalizedLoaded, Router } from 'vue-router';
 import CheckClose from '@/components/CheckClose.vue';
 import ErrorComponent from '@/components/ErrorComponent.vue';
 import FormErrorsList from '@/components/FormErrorsList.vue';
-import LabelFromYup from '@/components/camposDeFormulario/LabelFromYup.vue';
+import SmaeLabel from '@/components/camposDeFormulario/SmaeLabel.vue';
 import LoadingComponent from '@/components/LoadingComponent.vue';
 import MigalhasDePão from '@/components/MigalhasDePao.vue';
 import SmaeLink from '@/components/SmaeLink.vue';
@@ -13,12 +19,6 @@ import selecionarMultiplasOpcoes from '@/diretivas/selecionarMultiplasOpcoes';
 import type { RequestS } from '@/helpers/requestS';
 import requestS from '@/helpers/requestS';
 import consoleNaTemplate from '@/plugins/consoleNaTemplate';
-import type { Store } from 'pinia';
-import { createPinia } from 'pinia';
-import {
-  createApp, markRaw, nextTick,
-} from 'vue';
-import type { RouteLocationNormalizedLoaded, Router } from 'vue-router';
 import App from './App.vue';
 import CabecalhoDePagina from './components/CabecalhoDePagina.vue';
 import { router } from './router';
@@ -152,7 +152,8 @@ app.component('CheckClose', CheckClose);
 app.component('ErrorComponent', ErrorComponent);
 app.component('FormErrorsList', FormErrorsList);
 app.component('SmaeFieldsetSubmit', SmaeFieldsetSubmit);
-app.component('LabelFromYup', LabelFromYup);
+app.component('LabelFromYup', SmaeLabel);
+app.component('SmaeLabel', SmaeLabel);
 app.component('LoadingComponent', LoadingComponent);
 app.component('MigalhasDePão', MigalhasDePão);
 app.component('MigalhasDePao', MigalhasDePão);
diff --git a/frontend/src/views/edicoesEmLote/obras/EdicoesEmLoteObrasConstruir.vue b/frontend/src/views/edicoesEmLote/obras/EdicoesEmLoteObrasConstruir.vue
index 1ed1e0972c..70c43c4f11 100644
--- a/frontend/src/views/edicoesEmLote/obras/EdicoesEmLoteObrasConstruir.vue
+++ b/frontend/src/views/edicoesEmLote/obras/EdicoesEmLoteObrasConstruir.vue
@@ -7,11 +7,12 @@ import {
   lazy,
 } from 'yup';
 import { format } from 'date-fns';
-import { useEdicoesEmLoteStore } from '@/stores/edicoesEmLote.store';
 import {
   Field, FieldArray, ErrorMessage, useForm,
 } from 'vee-validate';
 import { computed, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useEdicoesEmLoteStore } from '@/stores/edicoesEmLote.store';
 import { useAlertStore } from '@/stores/alert.store';
 import { useEquipamentosStore } from '@/stores/equipamentos.store';
 import { useGruposTematicosStore } from '@/stores/gruposTematicos.store';
@@ -21,11 +22,9 @@ import { usePortfolioObraStore } from '@/stores/portfoliosMdo.store';
 import { useEtapasProjetosStore } from '@/stores/etapasProjeto.store';
 import rawStatusObras from '@/consts/statusObras';
 import { obras as schemaObras } from '@/consts/formSchemas';
-import LabelFromYup from '@/components/camposDeFormulario/LabelFromYup.vue';
 import CabecalhoDePagina from '@/components/CabecalhoDePagina.vue';
 import CampoDinamico from '@/components/alteracaoEmLotes.componentes/CampoDinamico.vue';
 import SmaeFieldsetSubmit from '@/components/SmaeFieldsetSubmit.vue';
-import { useRoute, useRouter } from 'vue-router';
 
 const router = useRouter();
 const route = useRoute();
diff --git a/frontend/src/views/projetos/ProjetosCriarEditar.vue b/frontend/src/views/projetos/ProjetosCriarEditar.vue
index 078bf86162..1447d4bf67 100644
--- a/frontend/src/views/projetos/ProjetosCriarEditar.vue
+++ b/frontend/src/views/projetos/ProjetosCriarEditar.vue
@@ -1,4 +1,14 @@
 <script setup>
+import { storeToRefs } from 'pinia';
+import {
+  ErrorMessage,
+  Field,
+  FieldArray,
+  useForm,
+  useIsFormDirty,
+} from 'vee-validate';
+import { computed, ref, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import AutocompleteField from '@/components/AutocompleteField2.vue';
 import CampoDePessoasComBuscaPorOrgao from '@/components/CampoDePessoasComBuscaPorOrgao.vue';
 import CampoDePlanosMetasRelacionados from '@/components/CampoDePlanosMetasRelacionados.vue';
@@ -17,16 +27,6 @@ import { useObservadoresStore } from '@/stores/observadores.store.ts';
 import { useOrgansStore } from '@/stores/organs.store';
 import { usePortfolioStore } from '@/stores/portfolios.store.ts';
 import { useProjetosStore } from '@/stores/projetos.store.ts';
-import { storeToRefs } from 'pinia';
-import {
-  ErrorMessage,
-  Field,
-  FieldArray,
-  useForm,
-  useIsFormDirty,
-} from 'vee-validate';
-import { computed, ref, watch } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
 
 const baseUrl = `${import.meta.env.VITE_API_URL}`;
 
@@ -280,9 +280,7 @@ watch(itemParaEdicao, (novoValor) => {
 
     <hr class="f1">
 
-    <MenuDeMudançaDeStatusDeProjeto
-      v-if="projetoId"
-    />
+    <MenuDeMudançaDeStatusDeProjeto v-if="projetoId" />
 
     <CheckClose :formulario-sujo="formularioSujo" />
   </header>
@@ -292,7 +290,6 @@ watch(itemParaEdicao, (novoValor) => {
     @submit.prevent="!isSubmitting ? onSubmit() : null"
   >
     <div class="flex g2 mb1">
-
       <div class="f1 mb1">
         <LabelFromYup
           name="portfolio_id"
@@ -310,7 +307,10 @@ watch(itemParaEdicao, (novoValor) => {
               <svg
                 width="10"
                 height="10"
-              ><use xlink:href="#i_edit" /></svg><div>Trocar de portfolio</div>
+              >
+                <use xlink:href="#i_edit" />
+              </svg>
+              <div>Trocar de portfolio</div>
             </router-link>
           </template>
         </LabelFromYup>
@@ -395,9 +395,7 @@ watch(itemParaEdicao, (novoValor) => {
           :class="{ error: errors.status }"
           :disabled="!emFoco?.permissoes.status_permitidos?.length"
         >
-          <option
-            :value="null"
-          >
+          <option :value="null">
             Selecionar
           </option>
           <option
@@ -453,12 +451,12 @@ watch(itemParaEdicao, (novoValor) => {
           :schema="schema"
         />
         <SmaeText
+          v-model="values.resumo"
           name="resumo"
           as="textarea"
           rows="5"
           class="inputtext light mb1"
           maxlength="2048"
-          v-model="values.resumo"
           anular-vazio
           :class="{ 'error': errors.resumo }"
         />
@@ -477,12 +475,12 @@ watch(itemParaEdicao, (novoValor) => {
             :schema="schema"
           />
           <SmaeText
+            v-model="values.objeto"
             name="objeto"
             as="textarea"
             rows="5"
             class="inputtext light mb1"
             maxlength="2048"
-            v-model="values.objeto"
             anular-vazio
             :class="{ 'error': errors.objeto }"
             :disabled="!emFoco?.permissoes?.campo_objeto"
@@ -501,12 +499,12 @@ watch(itemParaEdicao, (novoValor) => {
             :schema="schema"
           />
           <SmaeText
+            v-model="values.objetivo"
             name="objetivo"
             as="textarea"
             rows="5"
             class="inputtext light mb1"
             maxlength="2048"
-            v-model="values.objetivo"
             anular-vazio
             :class="{ 'error': errors.objetivo }"
             :disabled="!emFoco?.permissoes?.campo_objetivo"
@@ -525,12 +523,12 @@ watch(itemParaEdicao, (novoValor) => {
             :schema="schema"
           />
           <SmaeText
+            v-model="values.publico_alvo"
             name="publico_alvo"
             as="textarea"
             rows="5"
             class="inputtext light mb1"
             maxlength="2048"
-            v-model="values.publico_alvo"
             anular-vazio
             :class="{ 'error': errors.publico_alvo }"
             :disabled="!emFoco?.permissoes?.campo_publico_alvo"
@@ -595,7 +593,9 @@ watch(itemParaEdicao, (novoValor) => {
               <svg
                 width="20"
                 height="20"
-              ><use xlink:href="#i_remove" /></svg>
+              >
+                <use xlink:href="#i_remove" />
+              </svg>
             </button>
           </div>
 
@@ -609,7 +609,9 @@ watch(itemParaEdicao, (novoValor) => {
             <svg
               width="20"
               height="20"
-            ><use xlink:href="#i_+" /></svg>Adicionar premissa
+            >
+              <use xlink:href="#i_+" />
+            </svg>Adicionar premissa
           </button>
         </FieldArray>
       </div>
@@ -668,7 +670,9 @@ watch(itemParaEdicao, (novoValor) => {
               <svg
                 width="20"
                 height="20"
-              ><use xlink:href="#i_remove" /></svg>
+              >
+                <use xlink:href="#i_remove" />
+              </svg>
             </button>
           </div>
 
@@ -682,7 +686,9 @@ watch(itemParaEdicao, (novoValor) => {
             <svg
               width="20"
               height="20"
-            ><use xlink:href="#i_+" /></svg>Adicionar restrição
+            >
+              <use xlink:href="#i_+" />
+            </svg>Adicionar restrição
           </button>
         </FieldArray>
       </div>
@@ -697,12 +703,12 @@ watch(itemParaEdicao, (novoValor) => {
           :schema="schema"
         />
         <SmaeText
+          v-model="values.principais_etapas"
           name="principais_etapas"
           as="textarea"
           rows="5"
           class="inputtext light mb1"
           maxlength="2048"
-          v-model="values.principais_etapas"
           anular-vazio
           :class="{ 'error': errors.principais_etapas }"
         />
@@ -725,17 +731,17 @@ watch(itemParaEdicao, (novoValor) => {
           Não escopo
           <small class="t13 tc500">(o que <strong>não</strong> será entregue no projeto)</small>
         </LabelFromYup>
-          <SmaeText
-            name="nao_escopo"
-            as="textarea"
-            rows="5"
-            class="inputtext light mb1"
-            maxlength="2048"
-            v-model="values.nao_escopo"
-            anular-vazio
-            :class="{ 'error': errors.nao_escopo }"
-            :disabled="!emFoco?.permissoes?.campo_nao_escopo"
-          />
+        <SmaeText
+          v-model="values.nao_escopo"
+          name="nao_escopo"
+          as="textarea"
+          rows="5"
+          class="inputtext light mb1"
+          maxlength="2048"
+          anular-vazio
+          :class="{ 'error': errors.nao_escopo }"
+          :disabled="!emFoco?.permissoes?.campo_nao_escopo"
+        />
         <ErrorMessage
           name="nao_escopo"
           class="error-msg"
@@ -880,7 +886,9 @@ watch(itemParaEdicao, (novoValor) => {
               <svg
                 width="20"
                 height="20"
-              ><use xlink:href="#i_remove" /></svg>
+              >
+                <use xlink:href="#i_remove" />
+              </svg>
             </button>
           </div>
 
@@ -897,7 +905,9 @@ watch(itemParaEdicao, (novoValor) => {
             <svg
               width="20"
               height="20"
-            ><use xlink:href="#i_+" /></svg>Adicionar fonte de recursos
+            >
+              <use xlink:href="#i_+" />
+            </svg>Adicionar fonte de recursos
           </button>
         </FieldArray>
       </div>
@@ -912,6 +922,7 @@ watch(itemParaEdicao, (novoValor) => {
             name="origem_tipo"
             :schema="schema"
           />
+
           <Field
             name="origem_tipo"
             as="select"
@@ -1598,9 +1609,7 @@ watch(itemParaEdicao, (novoValor) => {
           name="grupo_portfolio"
           class="error-msg"
         />
-        <ErrorComponent
-          :erro="erroNosGruposDeObservadores"
-        />
+        <ErrorComponent :erro="erroNosGruposDeObservadores" />
       </div>
     </div>
 


Reviews:
user: login: robsonsobral
body: Só um dos comentários é impeditivo.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T18:08:51Z
user: login: robsonsobral
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:29:41Z
user: login: robsonsobral
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:30:47Z
user: login: robsonsobral
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:45:35Z

Review comments:
diff_hunk: @@ -0,0 +1,131 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import removerHtml from '@/helpers/html/removerHtml';
+
+type Slots = {
+  default(): void
+  botao(): void
+};
+
+type Props = {
+  texto?: string
+  icone?: string
+};
+
+defineSlots<Slots>();
+withDefaults(defineProps<Props>(), {
+  icone: 'i',
+  texto: undefined,
+});
+
+const content = ref<HTMLElement>();
+const exibicaoTooltip = ref<boolean>(false);
+const manterExibido = ref<boolean>(false);
+
+const statusTooltip = computed<boolean>(() => {
+  if (manterExibido.value) {
+    return false;
+  }
+
+  return !exibicaoTooltip.value;
+});
+
+const descricaoConteudo = computed<string>(() => {
+  if (!content.value) {
+    return '';
+  }
+
+  return removerHtml(content.value.innerHTML);
path: frontend/src/components/SmaeTooltip/SmaeTooltip.vue
user: login: robsonsobral
body: Aqui você pode usar `content.value.textContent()`. Performará muito melhor.
created_at: 2025-04-29T18:05:19Z
author_association: COLLABORATOR
path: frontend/src/components/camposDeFormulario/SmaeLabel.vue
user: login: robsonsobral
body: Achei que você tinha mantido o nome velho. Agora há dois arquivos. Dará mais trabalho para trocar. Mas é você quem sabe. Não se esqueça de que o Dudu também está usando esse arquivo. Terá que esperar por ele para trocar.
created_at: 2025-04-29T18:08:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,131 @@
+<script lang="ts" setup>
+import { computed, ref } from 'vue';
+import removerHtml from '@/helpers/html/removerHtml';
+
+type Slots = {
+  default(): void
+  botao(): void
+};
+
+type Props = {
+  texto?: string
+  icone?: string
+};
+
+defineSlots<Slots>();
+withDefaults(defineProps<Props>(), {
+  icone: 'i',
+  texto: undefined,
+});
+
+const content = ref<HTMLElement>();
+const exibicaoTooltip = ref<boolean>(false);
+const manterExibido = ref<boolean>(false);
+
+const statusTooltip = computed<boolean>(() => {
+  if (manterExibido.value) {
+    return false;
+  }
+
+  return !exibicaoTooltip.value;
+});
+
+const descricaoConteudo = computed<string>(() => {
+  if (!content.value) {
+    return '';
+  }
+
+  return removerHtml(content.value.textContent);
path: frontend/src/components/SmaeTooltip/SmaeTooltip.vue
user: login: robsonsobral
body: Esqueceu aqui!
created_at: 2025-04-30T16:30:42Z
author_association: COLLABORATOR

PR comments:
user: login: cloudflare-workers-and-pages[bot]
created_at: 2025-04-29T17:33:37Z
body: ## Deploying smae with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>b1c52b5</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://da279bb7.smae.pages.dev'>https://da279bb7.smae.pages.dev</a>
</td></tr>
<tr><td><strong>Branch Preview URL:</strong></td><td>
<a href='https://feature-smaetooltip.smae.pages.dev'>https://feature-smaetooltip.smae.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/32164cbf7c98d775eb838d1a08ee2a8e/pages/view/smae/da279bb7-76e5-4a15-8f63-9a3f1c4f3eff)

author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-30T16:45:20Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=AppCivico_smae&pullRequest=263) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=AppCivico_smae&pullRequest=263&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=AppCivico_smae&pullRequest=263&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=AppCivico_smae&pullRequest=263&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=AppCivico_smae&pullRequest=263&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=AppCivico_smae&pullRequest=263&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=AppCivico_smae&pullRequest=263)


author_association: NONE