Appearance
Code Style - Padrões de Código
Estrutura de módulos
OBRIGATÓRIO: Todo módulo em src/modules/<mod>/ tem exatamente estas subpastas:
src/modules/<mod>/
├── services/ # camada de API (getXxxService, postXxxService)
├── hooks/ # stores Pinia + composables (useXxx.ts)
├── components/ # componentes Vue do módulo
├── pages/ # páginas Vue do módulo
└── router/ # rotas do móduloRegras inegociáveis
| Regra | Descrição |
|---|---|
PROIBIDO stores/ | Pastas stores/ não existem. Stores Pinia vivem em hooks/useXxx.ts |
PROIBIDO composables/ | Pastas composables/ não existem. Composables vivem em hooks/useXxx.ts |
| Hooks unificados | Stores e composables compartilham a mesma pasta hooks/ e o mesmo padrão de nomenclatura useXxx.ts |
Sem sufixo Store | NUNCA usar useXxxStore. O nome do hook é sempre useXxx, mesmo quando for defineStore |
defineStore interno | export const useCompliance = defineStore('compliance', () => { ... }) o primeiro argumento do defineStore (o id) pode ter qualquer nome, mas o export é sempre useXxx |
| Split obrigatório | PROIBIDO referenciar store.x no template ou no corpo do script. Sempre destruturar. Segurar o instance uma única vez apenas para destruturar (const myStore = useXxx(); const { action } = myStore; const { ref } = storeToRefs(myStore)) é válido, desde que myStore não seja usado após isso. Padrão de referência: src/modules/contracts/components/Create/Flow/Review/ContactReview.vue |
storeToRefs sempre | Para qualquer ref/computed de store Pinia, usar storeToRefs(). Ações são desestruturadas diretamente da store |
Sem reactive({}) wrapping | PROIBIDO retornar reactive({ ...storeState, ...actions }) em hooks. Devolver objeto plano preservando as refs |
Exemplo correto
typescript
// ✅ CORRETO - src/modules/compliance/hooks/useCompliance.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCompliance = defineStore('compliance', () => {
const items = ref<ComplianceFlow[]>([])
const loading = ref(false)
const activeItems = computed(() => items.value.filter((i) => i.status === 'active'))
const loadItems = async () => { /* ... */ }
return { items, loading, activeItems, loadItems }
})vue
<!-- ✅ CORRETO - consumo em uma página -->
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCompliance } from '@/modules/compliance/hooks/useCompliance'
const { items, loading, activeItems } = storeToRefs(useCompliance())
const { loadItems } = useCompliance()
loadItems()
</script>Exemplo errado (tudo que a IA já fez e não pode voltar a fazer)
typescript
// ❌ ERRADO - sufixo Store, pasta stores/, sem split, sem storeToRefs
// src/modules/compliance/stores/compliance.ts
export const useComplianceStore = defineStore('compliance', () => { /* ... */ })
// consumo errado:
const store = useComplianceStore()
// template: store.items, store.loading, store.activeItems
// ❌ ERRADO - composable wrappando store e devolvendo reactive({})
export const useComplianceDetail = () => {
const store = useComplianceDetailStore()
return reactive({
flow: store.flow,
loading: store.loading,
handleSave
})
}Antes de criar um hook novo
greppelo conceito no repositório inteiro:useXxxpode já existir em outro módulo- Se existir, reutilizar. Nunca duplicar serviço ou hook de listagem (ex: providers)
- Ler
project-structure.mdecode-style.mdantes de propor novo padrão
Uma função por arquivo de hook
OBRIGATÓRIO: Um arquivo de hook (useXxx.ts) pode conter apenas uma função no nível do módulo: o próprio hook. PROIBIDO declarar funções auxiliares (pure helpers, adapters, formatters, etc.) no escopo do módulo do mesmo arquivo.
Regras
| Regra | Descrição |
|---|---|
| Apenas o hook no módulo | Nenhuma const foo = (...) => ... ou function foo(...) no topo do arquivo além do export const useXxx = ... |
| Funções internas do hook são permitidas | Arrow functions atribuídas a const dentro do corpo do hook (actions, handlers, computeds) são parte do hook, não violam a regra |
| Constantes estáticas são permitidas | const FOO_MAP = {...}, const LIMIT = 42 no topo do arquivo são OK a restrição é sobre funções |
| Onde colocar helpers extraídos | Pure domain helpers → src/domain/<mod>/<nome>.ts. Helpers UI-only → src/modules/<mod>/components/<Componente>/helpers.ts ou arquivo sibling não-hook em hooks/ (sem prefixo use). Nunca no arquivo do hook |
Motivação
Hook files são pontos de composição: misturar helpers pure com orquestração reativa dificulta a leitura, infla o arquivo e mascara violações de max-lines-per-function. Separar helpers em arquivos dedicados deixa o hook enxuto e os helpers testáveis isoladamente.
Exemplo
typescript
// ❌ ERRADO - helpers misturados no hook file
// src/modules/contracts/hooks/useContractCompliance.ts
const parseFlowList = <T>(raw: unknown): T[] => { ... }
const mergeCreatedFlows = (current, created) => { ... }
const buildInitialFlowMap = (providers, flows) => { ... }
export const useContractCompliance = defineStore('contractCompliance', () => {
// ... usa os helpers acima
})
// ✅ CORRETO - helpers no domain, hook só compõe
// src/domain/contract/compliance.ts
export const parseFlowList = <T>(raw: unknown): T[] => { ... }
export const mergeCreatedFlows = (current, created) => { ... }
export const buildInitialFlowMap = (providers, flows) => { ... }
// src/modules/contracts/hooks/useContractCompliance.ts
import { parseFlowList, mergeCreatedFlows, buildInitialFlowMap } from '@/domain/contract/compliance'
export const useContractCompliance = defineStore('contractCompliance', () => {
// usa os helpers importados
})Hooks de módulo não recebem parâmetros
OBRIGATÓRIO: Hooks/composables de módulo (src/modules/<mod>/hooks/) não têm parâmetros. Todo estado vem do store; um elemento DOM entra por uma ação (setMeasurer(el), setOverlay(el)), nunca como argumento do hook. Padrões como useStructurePagination(refs), useVariableParser(content) ou usePlacementOverlay(overlayRef) são proibidos em hooks de módulo.
Exceção: componentes genéricos em src/components/
Componentes genéricos e reutilizáveis em src/components/ (ex: DocumentCanvas) são prop-driven e não têm store de módulo — e costumam ter múltiplas instâncias na mesma tela (preview + revisão + modal). Um store Pinia é singleton, então não serve (as instâncias colidiriam). Para esses casos, os composables co-localizados em src/components/<Componente>/podem receber as props como Ref:
typescript
// ✅ CORRETO - apenas para componentes genéricos prop-driven em src/components/
export const useDocumentPagination = (options: { html: Ref<string>; margins?: Ref<...> }) => { ... }
// no componente
const { isPaginated, pages } = useDocumentPagination({ html: toRef(props, 'html'), ... })Regras que continuam valendo mesmo nessa exceção: um hook por arquivo (sem const/método/ type/interface entre imports e o export const useXxx — constantes vão pra dentro do corpo; tipos de parâmetro são inline na assinatura), sem tipar retorno, sem lifecycle no hook (expõe init/reset, o componente pluga), e o componente segue pura view. A exceção é restrita a src/components/ genéricos; hooks de módulo continuam sem parâmetros.
Inversão de dependências para hooks complexos
OBRIGATÓRIO: Quando um hook/store fica complexo demais (warnings de max-lines-per-function: função acima de 100 linhas: ou complexidade ciclomática alta), NÃO esconder a complexidade dentro de métodos auxiliares no mesmo arquivo. A resposta correta é inverter as dependências: quebrar o hook em múltiplos hooks menores, cada um com uma responsabilidade única.
Regras
| Regra | Descrição |
|---|---|
| Um hook, uma responsabilidade | Se o hook tem mais de ~8-10 ações ou mistura preocupações distintas (ex: estado + histórico + modal + compliance), separe em hooks independentes |
| Componentes consomem múltiplos hooks | É válido (e desejado) um componente puxar refs/ações de dois ou três hooks diferentes isso é preferível a consumir um único hook inchado |
| Dependência unidirecional | Hooks menores podem importar outros hooks (ex: useContractCompliance lê providers de useActiveContract), mas sem ciclos. Quando um hook "mãe" precisa resetar seus filhos, ele chama reset() de cada um explicitamente |
| Estado compartilhado fica no hook mais genérico | Dados que vários hooks precisam (ex: providers, toolbar) ficam no hook base; os hooks especializados leem via storeToRefs |
| Não esconda complexidade com wrappers | Não criar um hook "facade" que apenas reexporta tudo dos hooks menores isso recria o problema. O componente importa diretamente os hooks que precisa |
Quando aplicar
Gatilhos para inverter:
- Lint warning
max-lines-per-functionnodefineStore(() => { ... })ou composable - Warning de complexidade ciclomática (
complexity) - Mais de um "capítulo" lógico distinto no mesmo hook (ex: dados de listagem + estado de modal + fluxo de edição)
- Funcs auxiliares com escopo interno que só existem por causa de um único fluxo
Exemplo
typescript
// ❌ ERRADO - um store gigante com múltiplas responsabilidades
export const useActiveContract = defineStore('activeContract', () => {
// 40 linhas de estado do contrato
// 60 linhas de histórico
// 120 linhas de compliance modal
// 80 linhas de transições
// → warning max-lines-per-function (300 linhas)
})
// ✅ CORRETO - hooks especializados, consumidos em conjunto
// useActiveContract.ts base (< 100 linhas)
export const useActiveContract = defineStore('activeContract', () => {
const activeSidebarPanel = ref(...)
const toolbar = computed(() => contractDetail.toolbar)
// transições, navegação de painel, display helpers
})
// useContractHistory.ts histórico isolado
export const useContractHistory = defineStore('contractHistory', () => {
const historyItems = ref([])
const loadHistory = () => { ... }
})
// useContractCompliance.ts modal de compliance isolado
export const useContractCompliance = defineStore('contractCompliance', () => {
const showAttachComplianceModal = ref(false)
const { providers } = storeToRefs(useActiveContract())
// form, pendingAssignments, handlers
})
// No componente
const { toolbar, statusType } = storeToRefs(useActiveContract())
const { historyItems } = storeToRefs(useContractHistory())
const { showAttachComplianceModal } = storeToRefs(useContractCompliance())Lifecycle hooks só em páginas/componentes
OBRIGATÓRIO: onMounted, onBeforeUnmount, onUnmounted, onActivated, onDeactivated etc. NUNCA vivem dentro de useXxx.ts (hook ou store). Lifecycle pertence ao componente Vue (página ou componente) que tem ciclo de vida, o hook expõe init(), reset() (e similares) e o consumidor pluga no ciclo.
Regras
| Regra | Descrição |
|---|---|
| Hook não importa lifecycle | Nada de import { onMounted } from 'vue' em hooks/useXxx.ts. Se o hook precisa de side-effects no mount, encapsule em init() |
Cleanup em reset() | O cleanup que iria em onBeforeUnmount/onUnmounted mora numa action reset() retornada pelo hook |
| Página/componente pluga o ciclo | const { init, reset } = useXxxPage(); onMounted(init); onBeforeUnmount(reset) |
| Vale para stores Pinia também | defineStore setup-style segue a mesma regra: expõe init/reset, sem onMounted no corpo |
| Watchers OK no hook | watch/watchEffect podem ficar no hook (não dependem do ciclo de vida do componente para setup) |
Motivação
Hook é orientado a estado e ações; ciclo de vida pertence ao componente que o consome. Embutir onMounted no hook acopla cedo demais e em remontagens (HMR, navegação, troca de rota com <component :key>) o efeito pretendido duplica ou roda em horas erradas. Mantendo lifecycle só na página, o ciclo de vida fica coerente e previsível.
Exemplo
typescript
// ❌ ERRADO - lifecycle dentro do hook
// src/modules/contracts/hooks/useDetailContract.ts
export const useDetailContract = () => {
// ...
onMounted(bootstrap)
onUnmounted(() => {
activeContract.reset()
compliance.reset()
})
return { openCancelModal }
}typescript
// ✅ CORRETO - hook expõe init/reset, página pluga
// src/modules/contracts/hooks/useDetailContract.ts
export const useDetailContract = () => {
// ...
const init = () => {
bootstrap()
}
const reset = () => {
activeContract.reset()
compliance.reset()
}
return { openCancelModal, init, reset }
}vue
<!-- src/modules/contracts/pages/DetailContract.vue -->
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue'
import { useDetailContract } from '@/modules/contracts/hooks/useDetailContract'
const { init, reset } = useDetailContract()
onMounted(init)
onBeforeUnmount(reset)
</script>Estrutura de Componentes Vue
OBRIGATÓRIO: Seguir a ordem <template>, <script setup>, <style>
vue
<template>
<!-- Template HTML -->
</template>
<script setup lang="ts">
// Lógica do componente
</script>
<style lang="scss" scoped>
// Estilos SCSS
</style>Regras de Código
| Regra | Descrição |
|---|---|
| Sem comentários | Não adicionar comentários no código. O código deve ser autoexplicativo |
| Script Setup | Usar sempre <script setup lang="ts"> |
| TypeScript | Todo código deve ser tipado |
| Sem tipar retorno | NÃO tipar retorno de funções. O TypeScript infere automaticamente |
| Sem if inline | PROIBIDO if em uma linha. Sempre usar bloco { } com quebra de linha |
| Sem return inline | PROIBIDO return na mesma linha do if. Sempre dentro do bloco { } |
| Linha vazia após if | Sempre deixar uma linha vazia após o fechamento } de um bloco if |
| Linha vazia após const/let | Sempre deixar uma linha vazia após cada declaração const ou let |
| SCSS | Usar <style lang="scss" scoped> para estilos |
| Composables | Extrair lógica reutilizável em composables |
| Sem aninhamento | PROIBIDO aninhar if ou for. Usar early returns e métodos auxiliares |
| Responsabilidade única | Cada método deve ter apenas uma responsabilidade. Extrair lógica em funções menores |
| Sem lógica em componentes | Componentes são pura view. Toda lógica de negócio, filtros, computeds e ações deve ficar em stores ou composables |
| Lifecycle só em páginas/componentes | onMounted/onBeforeUnmount/onUnmounted etc. NUNCA dentro de hooks/useXxx.ts. Hook expõe init()/reset(); página chama onMounted(init) / onBeforeUnmount(reset) |
| Sem try/catch em componentes | Tratamento de erros deve ficar na store ou composable. Componentes apenas chamam métodos |
| storeToRefs + split | const { refs } = storeToRefs(useXxx()) para variáveis reativas + const { actions } = useXxx() para ações. PROIBIDO const store = useXxx() seguido de store.xxx no template/script |
| i18n em hooks/componentes | Usar const { t } = useI18n(). PROIBIDO i18n.global.t em hooks, stores e componentes. i18n.global.t é permitido apenas em arquivos de router/ (meta.title em lazy loading) e utils puros sem reatividade |
| Validação de form | Usar rules do n-form por campo, com mensagens específicas para cada regra. PROIBIDO validar manualmente campos obrigatórios com message.error genérico. message.error é reservado para erros de backend no .catch |
v-max-length obrigatório | Todo <n-input> e <n-textarea> que recebe texto livre DEVE usar a diretiva v-max-length="TEXT_LENGTH.*" (de @/domain/common/constants). PROIBIDO usar :maxlength HTML pode ser burlado via DevTools/paste. Search inputs usam TEXT_LENGTH.SEARCH. Detalhes em input-length-limits.md |
Sem reactive({}) em hooks | Hooks/composables retornam objeto plano de refs e ações. PROIBIDO return reactive({ ... }) wrappando estado já reativo causa dupla indireção e quebra storeToRefs |
| BEM no SCSS | Usar convenção BEM (block__element--modifier) com @apply Tailwind nos estilos SCSS |
| Sem cores hardcoded | Não usar cores fixas (rgba, #hex). Usar classes Tailwind com variantes dark: ou variáveis CSS do tema para suportar tema claro/escuro e customização futura |
| Sem props/emits em nested | Componentes nested dentro do mesmo módulo devem acessar a store diretamente via storeToRefs() em vez de receber dados por props/emits. Props/emits são para componentes genéricos reutilizáveis entre módulos |
| router.push pelo name | PROIBIDO usar router.push('/path') com path string. Sempre usar router.push({ name: 'route-name' }) pois o path pode mudar |
| Erros do backend no catch | No .catch() de chamadas de serviço, sempre exibir a mensagem vinda do backend: catch((e) => { message.error(e?.response?.data?.message) }). Não usar mensagens genéricas de i18n no catch, o backend já retorna mensagens assertivas |
Separação de responsabilidades: componentes vs stores
Componentes são pura view
Componentes não devem conter lógica de negócio. Toda lógica deve estar em stores ou composables.
typescript
// ❌ ERRADO - lógica no componente
const search = ref('')
const debouncedSearch = ref('')
const updateDebounced = debounce((v: string) => { debouncedSearch.value = v }, 500)
watch(search, (v) => updateDebounced(v))
const filtered = computed(() => items.filter(...))
const handleDelete = (id: string) => { dialog.warning({ onPositiveClick: () => store.delete(id) }) }
// ✅ CORRETO - tudo na store, componente só consome
const store = useItemsStore()
const { search, filteredItems, loading } = storeToRefs(store)
const { init, handleDelete } = storestoreToRefs para variáveis, desestruturação para ações
typescript
// ❌ ERRADO - acessar tudo via store.xxx
const store = useStore()
// template: store.loading, store.items, store.deleteItem()
// ✅ CORRETO - separar refs de ações
const store = useStore()
const { items, loading, search } = storeToRefs(store)
const { deleteItem, loadItems, init } = storeSem try/catch em componentes
typescript
// ❌ ERRADO - try/catch no componente
const handleSave = async () => {
saving.value = true
try {
await api.save(data)
message.success('Salvo')
} catch {
message.error('Erro')
} finally {
saving.value = false
}
}
// ✅ CORRETO - componente só chama a store
const handleSave = async () => {
await saveProfile()
}Princípios de Clean Code
Sem if inline e sem return inline
typescript
// ❌ ERRADO - if inline com return
if (!user) return null
if (items.length === 0) return
// ✅ CORRETO - bloco com chaves e quebra de linha
if (!user) {
return null
}
if (items.length === 0) {
return
}Linha vazia após if, const e let
typescript
// ❌ ERRADO - sem espaçamento
const user = getUser()
const name = user.name
if (!name) {
return
}
const formatted = formatName(name)
// ✅ CORRETO - linha vazia após cada const/let e após cada bloco if
const user = getUser()
const name = user.name
if (!name) {
return
}
const formatted = formatName(name)Não tipar retorno de funções
typescript
// ❌ ERRADO - retorno tipado manualmente
const getUser = (): User => { ... }
const isValid = (value: string): boolean => { ... }
// ✅ CORRETO - TypeScript infere o retorno
const getUser = () => { ... }
const isValid = (value: string) => { ... }PROIBIDO aninhamento de estruturas de controle
typescript
// ❌ ERRADO - if aninhado
const processItems = (items: Item[]) => {
if (items.length > 0) {
for (const item of items) {
if (item.active) {
// lógica
}
}
}
}
// ✅ CORRETO - early return + método auxiliar
const processItems = (items: Item[]) => {
if (items.length === 0) {
return
}
const activeItems = items.filter(item => item.active)
activeItems.forEach(processItem)
}
const processItem = (item: Item) => {
// lógica isolada
}Responsabilidade única por método
typescript
// ❌ ERRADO - múltiplas responsabilidades
const handleSubmit = async () => {
const isValid = validateForm()
if (!isValid) {
showError('Formulário inválido')
return
}
const data = transformData(formData)
await api.save(data)
showSuccess('Salvo!')
router.push('/list')
}
// ✅ CORRETO - responsabilidades separadas
const handleSubmit = async () => {
if (!validateForm()) {
return showValidationError()
}
await saveData()
navigateToList()
}
const showValidationError = () => showError('Formulário inválido')
const saveData = async () => {
const data = transformData(formData)
await api.save(data)
showSuccess('Salvo!')
}
const navigateToList = () => router.push('/list')Nomenclatura
| Tipo | Convenção | Exemplo |
|---|---|---|
| Componentes | PascalCase | ContractList.vue |
| Hooks (store ou composable) | camelCase com prefixo use, sem sufixo Store | useContracts.ts exportando useContracts |
| Services | camelCase com sufixo Service | getContractsService |
| Types/Interfaces | PascalCase | Contract, ContractPart |
| Arquivos de página | PascalCase | ListContracts.vue |
Padrão de Props e Emits
typescript
const props = defineProps<{
currentStep: number
title: string
}>()
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'setStep', index: number): void
}>()Padrão de Composables
typescript
export const useContracts = () => {
const items = ref<Contract[]>([])
const loading = ref(false)
const loadContracts = async () => {
loading.value = true
const { data } = await getContractsService()
items.value = data
loading.value = false
}
return {
items,
loading,
loadContracts
}
}Regras de i18n (Traduções)
| Regra | Descrição |
|---|---|
| Sentence case | Usar letra maiúscula apenas no início da frase e em nomes próprios. Nunca usar Title Case (maiúscula em todas as palavras) |
| Nomes próprios | Manter maiúsculas apenas em: siglas (CPF, CNPJ, FAQ), marcas (WhatsApp), títulos de documentos legais (Termos de Serviço, Política de Privacidade), categorias jurídicas (Pessoa Jurídica, Pessoa Física) |
Sentence case em traduções
❌ ERRADO - Title Case
"Novo Contrato"
"Total de Horas"
"Relatório Mensal"
"Histórico de Contratos"
✅ CORRETO - Sentence case
"Novo contrato"
"Total de horas"
"Relatório mensal"
"Histórico de contratos"
✅ CORRETO - Exceções (nomes próprios, siglas, títulos legais)
"Termos de Serviço"
"Política de Privacidade"
"Pessoa Jurídica"
"CPF do responsável"Limites de Caracteres em Inputs
OBRIGATÓRIO: Toda entrada de texto vinda do usuário (form, search, textarea) deve usar a diretiva v-max-length="TEXT_LENGTH.*". Front bloqueia digitação E intercepta paste/DOM-edit; backend rejeita payloads maiores via 400. Defesa em profundidade.
A diretiva v-max-length (registrada em src/main.ts) trunca o valor no estado, não só no atributo HTML. :maxlength puro é proibido porque pode ser burlado via DevTools.
A tabela canônica de tiers e o mapeamento campo → tier vive em input-length-limits.md. Os valores espelham o backend.
Em <n-input> / <n-textarea>
vue
<template>
<n-input
v-model:value="form.name"
v-max-length="TEXT_LENGTH.SHORT"
:placeholder="t('placeholder.name')"
/>
</template>
<script setup lang="ts">
import { TEXT_LENGTH } from '@/domain/common/constants'
</script>Em search inputs de listagem (TableHeader.vue)
vue
<n-input
v-model:value="search"
v-max-length="TEXT_LENGTH.SEARCH"
:placeholder="t('search.placeholder')"
/>Em FormRules (validação de submit)
Para campos críticos, complemente o :maxlength com regra max no FormRules:
typescript
const rules: FormRules = {
description: [
{ required: true, message: t('global.formErrorRequired') },
{ max: TEXT_LENGTH.XLONG, message: t('global.formErrorMaxLength', { max: TEXT_LENGTH.XLONG }) }
]
}Em editores ricos (htmlContent)
:maxlength não funciona em Tiptap/Quill. Valide .length no hook antes de submeter:
typescript
const isHtmlValid = computed(() => htmlContent.value.length <= TEXT_LENGTH.HTML)E desabilite o botão de salvar quando exceder o limite.
Checklist para Novos Componentes
- [ ] Usar
<script setup lang="ts"> - [ ] Não adicionar comentários no código
- [ ] Seguir ordem: template → script → style
- [ ] Usar
<style lang="scss" scoped> - [ ] Tipar todas as props e emits
- [ ] Usar composables para lógica reutilizável
- [ ] Seguir convenções de nomenclatura
- [ ] Usar classes Tailwind para layout básico
- [ ] Usar i18n para textos estáticos
- [ ] Aplicar
v-max-length="TEXT_LENGTH.*"em todo<n-input>/<n-textarea>de texto livre