Brand tokens
Four hex values and one gradient derive all the CSS through color-mix; swapping the palette means editing one block.
The EchoSistema starter guide: how to run it, where everything lives and how to use the system's tokens and components.
The Nuxt starter for the group's platform sites: the whole Liquid Glass parameterized by four brand tokens.
Four hex values and one gradient derive all the CSS through color-mix; swapping the palette means editing one block.
Automatic parity: every themeable value has a dark twin, and the right theme paints before first paint.
Canonical Spanish at the root, pt-BR and English prefixed, a single catalog and locales in lockstep.
The browser's dialog, details and selects dressed by the system: accessibility for free.
pnpm installs, runs and validates. The same scripts as package.json, no hidden steps.
| Command | Role |
|---|---|
pnpm dev | Development server with hot reload. |
pnpm build | Production build (SSR). |
pnpm generate | Prerendered static site. |
pnpm preview | Serves the production build locally. |
pnpm lint | ESLint across the whole project. |
pnpm typecheck | vue-tsc over the app and the configuration. |
Standard Nuxt 4 layout: app/ is the site, i18n/ the texts, shared/ what server and client share.
app/ assets/css/main.css components/ composables/ data/ layouts/ pages/ plugins/ utils/ i18n/locales/ shared/ nuxt.config.ts
main.css is the single style entry point: .vue files load no style blocks. The language catalog lives in shared/languages.ts and the navigation in app/data/nav.ts.
The MARCA block at the top of main.css is the only place with brand hex values.
Backgrounds, text, glass, buttons and the code panel all derive from these four tokens through color-mix, with automatic light/dark parity. To rebrand a derived site, swap this block and nothing else; the agro and consumer palettes sit commented in the file as examples.
:root {
--brand-1: #426ab2;
--brand-2: #40c1c3;
--brand-deep: #040b2d;
--brand-link: #3d91cf;
}The theme flips on html[data-theme]; the language owns the URL.
data-theme
A pre-paint script in the head reads the stored preference (or the system's) and sets data-theme before first paint: no flash of the wrong theme. Runtime switching is the same attribute.
prefix_except_default
Canonical Spanish lives unprefixed; /pt-BR and /en prefix the other routes. Adding a language is one entry in the shared/languages.ts catalog plus the matching locale file.
One ruler for the masthead, the pages and the footer - and three values for it.
Everything that uses `.es-shell` follows the same ruler, which is why the reader’s preference moves the bar and the page together. Before the class existed each file wrote its own, and the home page was born narrower than the bar above it. Docs and the panel are ALWAYS full width and say so with `.es-shell--full`.
| Value | Ruler |
|---|---|
compact | 90rem · the reading column |
extended | 110rem · what the template ships with |
full | - · no ceiling, to the edges |
# .env - a específica vence a genérica
NUXT_TEMPLATE_THEME_WIDTH=extended
NUXT_THEME_WIDTH=extendedThe value is baked at build time: it travels in the script that paints the width before the first frame, and a width arriving after hydration is a layout jump in the reader’s face. After changing .env, rebuild. The floating gear switches per reader, and that choice lives in their browser.
Semantic content is dressed by its container: .es-prose styles the elements inside it.
Headings, paragraphs, quotations, code and the other text elements pick up the system's style by entering an .es-prose, with no per-element utilities. The loaded families are Manrope, Montserrat and JetBrains Mono.
<div class="es-prose">
<h2>…</h2>
<p>…</p>
<blockquote>…</blockquote>
</div>The 41 components in app/components/echo, by role. Each dresses a native element when one exists: that is where keyboard, focus and announcement come from, with no reimplementation.
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
Calendar · CalendarMini · app/utils/calendar.ts
LoginForm · RegisterForm · ForgotPasswordForm · UpdatePasswordForm · PageShell · LoginModal · RegisterModal · ForgotPasswordModal · UpdatePasswordModal · LockModal · LoginPrompt
MastheadEcommerce · MastheadRealEstate · MastheadLearn · MastheadHelpDesk · MastheadLanding · FooterEcommerce · FooterRealEstate · FooterLearn · FooterHelpDesk · FooterLanding · SidebarPanel · SidebarDocs · SidebarFilters · ElementThemeSwitch · ElementLanguageSwitch · ElementCurrencySwitch · ElementSwitchDropdown
Two axes never combined by hand: .es-i--* provides the color, the variant consumes it.
Ten intents define local tokens (--i, --i-on, --i-grad and derivatives) consumed by the four label families and the modal. The variants (filled, gradient, outline, glass, ghost) are each written once.
<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>Eight tones over the same palette, with the accent wedge on the edge.
Four semantic status tones (info, success, warn, danger) and four named tonal ones (caution, steel-blue, navy-blue, dark). Text stays in the theme's grays; the accent goes into the icon, the title and the wedge.
<echo-alert tone="success" title="…">
…
</echo-alert>The native dialog: trapped focus, Esc, inert and blurred background.
An opaque theme surface, an intent wedge like the alerts, a background blurred into illegibility while open and locked scrolling. Clicking outside does not close; Esc, the header close and the footer actions do. Width follows the sm, md, lg, xl and xxl scale.
<echo-modal ref="modal" title-id="…"
intent="warning" size="lg">
<template #title>…</template>
…
<template #footer>…</template>
</echo-modal>
modal.value?.open()The shortest line that puts each component on screen, ready to copy.
Every component carries the same snippet in its own file, in a "Copy and paste" block right above the code - that is where the full prop list lives. What is here are the minimum usages, to find what fits without opening seven files.
<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-loader :loading="loading">…</echo-loader>
<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" /><echo-calendar v-model:view="view" v-model:date="day" :events="EVENTS" @create="onCreate" @open="onOpen" />
<echo-calendar-mini v-model="day" :events="EVENTS" /><auth-login-form :form-id="id" :pending="sending" :error="failure" @submit="signIn" />
<auth-register-form :form-id="id" @submit="register" @login="toLogin" />
<auth-forgot-password-form :form-id="id" :sent="sent" @submit="recover" />
<auth-update-password-form :form-id="id" require-current @submit="changePassword" />
<auth-page-shell :title="title" :image="COVER" cover-title="…">…</auth-page-shell>
<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" /><masthead-ecommerce :links="LINKS" :departments="DEPARTMENTS" :cart-count="3" @search="onSearch" />
<masthead-learn :links="LINKS" :course="course" :done="17" :total="24" />
<masthead-help-desk :links="LINKS" :status="status" status-tone="success" :ticket-label="label" />
<masthead-landing :links="SECTIONS" :cta-label="cta" floating />
<footer-ecommerce :columns="COLUMNS" :legal="LEGAL" phone="…" email="…" />
<footer-learn :columns="COLUMNS" :cta-title="…" :cta-label="…" /> · <footer-landing :links="LINKS" :tagline="…" />
<sidebar-panel v-model:collapsed="collapsed" :groups="GROUPS" />
<sidebar-docs :groups="NAV" content-selector="#guide" /> · <sidebar-filters v-model="picked" :groups="FILTERS" />
<element-theme-switch /> · <element-language-switch compact /> · <element-currency-switch />Sign in, create account, recover and change the password, and the locked session. Five ready components in app/components/auth/, over the same echo-modal.
All five follow the same contract: open() and close() exposed by ref, one submit with the data, and no network calls inside. The application knows the URL, the token and what to do with the response - the component only collects, validates what doesn't depend on the server, and hands it back.
<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()The submit comes out in the API's vocabulary, not the internal variables': sending the payload straight to the endpoint is the common case, and renaming fields in every derived site would be the same mapping written many times.
| Component | Submit fields | Events |
|---|---|---|
<auth-*-form> | os mesmos campos do modal correspondente | @submit + os do fluxo |
<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 |
The props are the same across all five, with two noted exceptions. All are optional.
logo
Header artwork, when the modal stands for a brand other than the site's. Without it, the site logo.
title
Your own title instead of the flow's default text.
pending
Locks the fields and swaps the button label while your call runs.
error
Server message, shown in an alert above the form. Whoever passed it clears it.
sent
Recovery modal only: swaps the form for the sent confirmation. It belongs to the caller, because whoever made the request is who knows it arrived.
name / avatar
Lock modal only: name and portrait of whoever's session is locked.
Fields over the lens ink and the native details as a card.
es-input covers input and textarea; es-select is the native select with the same ink. es-collapse dresses the browser's details: it opens, closes and stays accessible without a line of 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>The chrome of the four kinds of site the group builds, ready to wear.
The shape of the chrome is a derived site’s first decision and the most expensive to undo later: a shop needs search in the middle of the first strip, a course needs the reader’s progress, a help desk needs the service’s state and a landing page needs one CTA with nothing competing. Starting from the nearest of the four is cheaper than bending the generic bar into it.
<masthead-ecommerce
:links="LINKS"
:departments="DEPARTMENTS"
:cart-count="cart.length"
flat
@search="goToListing"
/>
<footer-ecommerce :columns="COLUMNS" :legal="LEGAL" />The five bars and the five footers follow the same contract, and it is what lets a site change variant without rewriting the page.
Shop, course, help desk and landing page. They are not skins of one bar: what changes is what occupies the middle of the first strip.
No variant holds text: the links, the departments and the columns arrive ready and translated. A bar with names written into it is a bar that gets edited instead of used.
The default is no capsule; `lens` brings it back, for a bar over artwork. The icon buttons and the menus change together (`useBarSkin`): mixing the two is worse than either.
Drops the `sticky` and nothing else, for mounting a bar inside a page. It is what makes the showcase samples the real bars rather than drawings of them.
The conventions that hold for every derived site.
No style blocks in .vue: utilities in the template and everything else in main.css.
Raw hex in a template is a finding; the whole brand lives in the MARCA block.
Every system component class and token uses the prefix, renameable in one global replace.
All text checked against the real field; accent color is not text color.
prefers-reduced-motion and prefers-reduced-transparency respected by every animation and every glass.
Canonical Spanish; every interface string through t(), contracts stay literal.
A downloadable document covering the whole system, written to be handed to an agent along with the task.
It carries the rules that apply before any line of code (colour only through tokens, two axes never combined by hand, native element first, text through t(), a component that does not fetch) and the catalogue of every family with its tag, props, events and verbatim snippets.
Every showcase page also has its own version, carrying that page’s family alone: it is the file to hand over when the task is one screen, not a site.
/agents/echosistema-ui.md # the whole system
/agents/buttons.md # one family per file
/agents/navbars.md
/agents/calendar.md …A question the documentation does not cover?
Talk to the EchoSistema group at echosistema.online.
What changed in the template, most recent first.