Tokens de marca
Quatro hex e um gradiente derivam todo o CSS por color-mix; trocar a paleta é editar um bloco.
O guia do starter EchoSistema: como rodar, onde cada coisa mora e como usar os tokens e componentes do sistema.
Starter Nuxt dos sites de plataforma do grupo: o Liquid Glass inteiro parametrizado por quatro tokens de marca.
Quatro hex e um gradiente derivam todo o CSS por color-mix; trocar a paleta é editar um bloco.
Paridade automática: todo valor tematizável tem gêmeo dark, e o tema certo pinta antes do primeiro paint.
Espanhol canônico na raiz, pt-BR e inglês prefixados, catálogo único e locales em lockstep.
Dialog, details e selects do navegador vestidos pelo sistema: acessibilidade de graça.
pnpm instala, roda e valida. Os mesmos scripts do package.json, sem passos escondidos.
| Comando | Papel |
|---|---|
pnpm dev | Servidor de desenvolvimento com hot reload. |
pnpm build | Build de produção (SSR). |
pnpm generate | Site estático pré-renderizado. |
pnpm preview | Serve o build de produção localmente. |
pnpm lint | ESLint no projeto inteiro. |
pnpm typecheck | vue-tsc sobre o app e a configuração. |
Layout padrão do Nuxt 4: app/ é o site, i18n/ os textos, shared/ o que servidor e cliente compartilham.
app/ assets/css/main.css components/ composables/ data/ layouts/ pages/ plugins/ utils/ i18n/locales/ shared/ nuxt.config.ts
main.css é o único ponto de entrada de estilo: arquivos .vue não carregam style. O catálogo de idiomas mora em shared/languages.ts e a navegação em app/data/nav.ts.
O bloco MARCA no topo do main.css é o único lugar com hex de marca.
Fundos, textos, vidro, botões e o painel de código derivam destes quatro tokens via color-mix, com paridade claro/escuro automática. Para rebatizar um site derivado, troque este bloco e nada mais; as paletas agro e consumer ficam comentadas no arquivo como exemplo.
:root {
--brand-1: #426ab2;
--brand-2: #40c1c3;
--brand-deep: #040b2d;
--brand-link: #3d91cf;
}O tema vira em html[data-theme]; o idioma é dono da URL.
data-theme
Um script pré-paint no head lê a preferência guardada (ou a do sistema) e define data-theme antes do primeiro paint: sem flash de tema errado. A troca em runtime é o mesmo atributo.
prefix_except_default
O espanhol canônico vive sem prefixo; /pt-BR e /en prefixam as demais rotas. Adicionar um idioma é uma entrada no catálogo de shared/languages.ts mais o arquivo de locale correspondente.
Conteúdo semântico se veste pelo contêiner: .es-prose estiliza os elementos de dentro.
Títulos, parágrafos, citações, código e os demais elementos de texto ganham o estilo do sistema ao entrar numa .es-prose, sem utility por elemento. As famílias carregadas são Manrope, Montserrat e JetBrains Mono.
<div class="es-prose">
<h2>…</h2>
<p>…</p>
<blockquote>…</blockquote>
</div>Os 41 componentes de app/components/echo, por papel. Cada um veste um elemento nativo quando existe um: é daí que vêm teclado, foco e anúncio, sem reimplementação.
Button · Chip · Badge · Pill · Dropdown · Pagination · Breadcrumb · Tabs
Field · Input · Textarea · Select · SearchSelect · TagsInput · Checkbox · Radio · Switch · Slider · Rating · FileButton · Dropzone · DatePicker · PhoneInput
Alert · Modal · Drawer · Toaster · Block · Skeleton · Spinner · Progress · Ring
Card · List · ListItem · DataTable · Avatar · Gallery · Tree · Timeline · Collapse · Divider · Stat · Player
Chart · Map · Carousel · Editor · ImageCropper · ImageViewer · QrCode · Barcode
Dois eixos que nunca se combinam à mão: .es-i--* dá a cor, a variante consome.
Dez intenções definem tokens locais (--i, --i-on, --i-grad e derivados) que as quatro famílias de rótulo e o modal consomem. As variantes (filled, gradient, outline, glass, ghost) são escritas uma vez cada.
<echo-button variant="filled" intent="danger">…</echo-button>
<echo-chip variant="soft" intent="info">…</echo-chip>
<echo-badge variant="filled" intent="success">…</echo-badge>
<echo-pill variant="outline" intent="caution">…</echo-pill>Oito tons sobre a mesma paleta, com a cunha de acento na borda.
Quatro semânticos de status (info, success, warn, danger) e quatro tonais nomeados (caution, steel-blue, navy-blue, dark). O texto fica nos cinzas do tema; o acento entra no ícone, no título e na cunha.
<echo-alert tone="success" title="…">
…
</echo-alert>O dialog nativo: foco preso, Esc, fundo inerte e embaçado.
Superfície opaca do tema, cunha de intenção como nos alerts, fundo ilegível pelo blur enquanto aberto e rolagem travada. Clique fora não fecha; fecham o Esc, o fechar do cabeçalho e as ações do rodapé. A largura segue a escala sm, md, lg, xl e xxl.
<echo-modal ref="modal" title-id="…"
intent="warning" size="lg">
<template #title>…</template>
…
<template #footer>…</template>
</echo-modal>
modal.value?.open()A linha mais curta que põe cada componente na tela, pronta para copiar.
Cada componente carrega o mesmo trecho no próprio arquivo, num bloco "Copy and paste" logo acima do código — é lá que está a lista completa de props. Aqui ficam só os usos mínimos, para achar o que serve sem abrir sete arquivos.
<echo-button variant="filled" intent="primary">Save</echo-button>
<echo-chip>tag</echo-chip> · <echo-badge intent="success">ok</echo-badge> · <echo-pill>4</echo-pill>
<echo-dropdown label="Account"><template #trigger>…</template>…</echo-dropdown>
<echo-tabs v-model="tab" :tabs="TABS" />
<echo-pagination v-model="page" :pages="12" />
<echo-breadcrumb :items="TRAIL" /><echo-field v-slot="{ id, fieldName }" label="E-mail" name="email"><echo-input :id="id" v-model="email" :name="fieldName" /></echo-field>
<echo-input v-model="text" type="email" size="sm" />
<echo-textarea v-model="note" rows="3" />
<echo-select v-model="topic"><option value="a">A</option></echo-select>
<echo-search-select v-model="country" list-id="countries" :options="names" />
<echo-date-picker v-model="birthday" mode="date" :start-year="1990" />
<echo-phone-input v-model="phone" country="br" @validity="ok = $event" />
<echo-checkbox v-model="agreed">I agree</echo-checkbox>
<echo-radio v-model="skin" name="skin" value="bordered" />
<echo-switch v-model="on" />
<echo-slider v-model="zoom" :min="1" :max="4" :step="0.01" />
<echo-rating v-model="stars" />
<echo-tags-input v-model="tags" />
<echo-file-button accept="image/*" @files="onFiles">Choose</echo-file-button>
<echo-dropzone accept="image/*" @files="onFiles">…</echo-dropzone><echo-alert tone="danger" title="That failed">…</echo-alert>
<echo-modal ref="el" title-id="x-title" size="md"><template #title>…</template>…</echo-modal>
<echo-drawer ref="el" title-id="y-title">…</echo-drawer>
useToast().push({ tone: 'success', title: 'Done', message: 'Saved.' })
<echo-block :loading="pending" label="Loading…">…</echo-block>
<echo-skeleton :lines="3" /> · <echo-spinner size="sm" />
<echo-progress :value="64" label="Progress" /> · <echo-ring :value="76" /><echo-card title="Team" subtitle="Who works here" :loading="pending">…</echo-card>
<echo-list><echo-list-item title="Row" /></echo-list>
<echo-data-table :rows="ROWS" :columns="COLS" />
<echo-avatar :src="url" name="Ada Lovelace" size="lg" />
<echo-gallery :items="IMAGES" />
<echo-tree :nodes="NODES" /> · <echo-timeline :items="STEPS" />
<echo-collapse><template #summary>…</template>…</echo-collapse>
<echo-stat label="Visits" value="24.8k" :delta="12.4" />
<echo-player :src="video" /><echo-chart :option="option" summary="What the chart says" height="280px" />
<echo-map :lat="-25.5" :lng="-54.6" :zoom="12" :markers="PINS" />
<echo-carousel label="Slides" loop autoplay>…</echo-carousel>
<echo-editor v-model="html" label="Body" />
<echo-image-cropper ref="el" :usages="['avatar']" mask="square" @apply="upload" />
<echo-image-viewer ref="el" :src="url" :caption="url" />
<echo-qr-code value="https://example.com" :size="180" />
<echo-barcode value="ECHO-2026-0001" format="CODE128" /><auth-login-modal ref="el" :pending="sending" :error="failure" @submit="signIn" />
<auth-register-modal ref="el" @submit="register" @login="switchToLogin" />
<auth-forgot-password-modal ref="el" :sent="sent" @submit="recover" />
<auth-update-password-modal ref="el" require-current @submit="changePassword" />
<auth-lock-modal ref="el" :name="user.name" :avatar="user.avatar" @submit="unlock" />Entrar, criar conta, recuperar e trocar a senha, e a sessão bloqueada. Cinco componentes prontos em app/components/auth/, sobre o mesmo echo-modal.
Os cinco seguem o mesmo contrato: open() e close() expostos por ref, um submit com os dados, e nada de rede por dentro. Quem sabe a URL, o token e o que fazer com a resposta é a aplicação — o componente só recolhe, valida o que não depende do servidor e devolve.
<auth-login-modal
ref="login"
:pending="sending"
:error="failure"
logo="/img/minha-marca.svg"
title="Entrar no painel"
@submit="signIn"
@forgot="forgot?.open()"
@register="register?.open()"
/>
login.value?.open()O submit sai no vocabulário da API, não no das variáveis internas: mandar o payload direto ao endpoint é o caso comum, e traduzir nome de campo em cada site derivado seria o mesmo de-para escrito muitas vezes.
| Componente | Campos do submit | Eventos |
|---|---|---|
<auth-login-modal> | email, password, remember | @submit @forgot @register |
<auth-register-modal> | name, email, gender (m|f|o), birth_date (yyyy-mm-dd), password, password_confirmation, terms (1|0) | @submit @login |
<auth-forgot-password-modal> | email | @submit @login |
<auth-update-password-modal> | password, password_confirmation | @submit |
<auth-lock-modal> | password | @submit @sign-out |
As props são as mesmas nos cinco, com duas exceções anotadas. Todas são opcionais.
logo
Arte do cabeçalho, quando o modal representa outra marca que não a do site. Sem ela, o logo do site.
title
Título próprio no lugar do texto padrão do fluxo.
pending
Trava os campos e troca o rótulo do botão enquanto a sua chamada corre.
error
Mensagem do servidor, exibida em alerta acima do formulário. Quem a limpa é quem a passou.
sent
Só no de recuperação: troca o formulário pela confirmação de envio. É do chamador, porque quem sabe que o pedido chegou é quem o fez.
name / avatar
Só no de bloqueio: nome e retrato de quem está com a sessão travada.
Campos sobre a tinta da lente e o details nativo de cartão.
es-input serve input e textarea; es-select é o select nativo com a mesma tinta. O es-collapse veste o details do navegador: abre, fecha e é acessível sem uma linha de JS.
<echo-input v-model="email" type="email" />
<echo-textarea v-model="message" rows="3" />
<echo-select v-model="topic">…</echo-select>
<echo-collapse>
<template #summary>…</template>
…
</echo-collapse>As convenções que valem para todo site derivado.
Nenhum bloco style em .vue: utility no template e o resto no main.css.
Hex cru em template é achado; a marca inteira vive no bloco MARCA.
Toda classe componente e token do sistema usa o prefixo, renomeável num replace global.
Todo texto verificado contra o campo real; cor de acento não é cor de texto.
prefers-reduced-motion e prefers-reduced-transparency respeitados por toda animação e vidro.
Espanhol canônico; toda string de interface via t(), contratos ficam literais.
Dúvida que a documentação não cobre?
Fale com o grupo EchoSistema em echosistema.online.
O que mudou no template, do mais recente para trás.