Ai

DESIGN.md

The visual contract of EletroDS. Color, spacing, typography, tokens for both the Vue and the React stack, tone of voice and logo usage. This is the file an AI agent should read before generating any Mercado Eletrônico interface.
This page is the canonical file. Its source is docs/content/3.ai/designmd.md in the eds-next repository, and DESIGN.md at the repository root is only a pointer to it, so there is a single copy to keep current. Product copy examples in the tone of voice section stay in Brazilian Portuguese on purpose: they are the artifact being documented, not prose.

Persistent context for any AI agent (Claude, Cursor, Copilot and so on) that generates or edits Mercado Eletrônico (ME) interfaces. It covers the product: color, spacing, typography, Vue and React tokens, tone of voice and the logo. It does not cover institutional marketing material (brand book, presentations), only the use of the logo inside the product (section 12).

Status: public, no authentication. Canonical location: eletro.design/ai/designmd. Maintained by the Eletro team (see section 0.2). Last verified against the code: 2026-08-21 (@nuxt/ui 4.10.0, tailwindcss 4.3.3, @mercadoeletronico/eds-next-react 1.1.1). An outdated file is worse than no file, because it leads the agent into error instead of flagging the gap, so treat every section marked "known gap" as exactly that, a real gap, not a licence to invent a value.


0. How to use this file

  1. Re-read section 4 (Absolute rules) before writing any component, screen or snippet.
  2. Identify the target: EDSnext (Vue 3 / Nuxt UI v3), EDSlegacy (Vue 2) or EDSreact (@mercadoeletronico/eds-next-react). Token naming, the dark mode mechanism and the way you style all differ between them. Section 2 has the full table, section 10 has the React-specific rules.
  3. Never hardcode a color, spacing, radius or font value. Always a token, even though the name differs between Vue and React.

0.2 Who maintains this and how to contribute

The EDS product (the name did not change) is maintained by the Eletro team (previously called "EDS"/"EletroDS" as a team name, renamed in 2026). The design system is innersource: any team at ME can open an improvement MR on the repositories (eds-next, eds-next-react, me-toolkit), and reviewing those contributions is the Eletro team's responsibility. In practice: an agent producing a contribution from outside the Eletro team follows the same rules in this file and waits for an Eletro review before merging, rather than skipping review for not being on the team.


1. What the product is for

ME (Mercado Eletronico) is a technology company offering corporate procurement solutions. The end user of the product interface is mostly:

  • A buyer or procurement professional inside a customer company, using the product daily for quotes, suppliers and approvals.
  • A registered supplier, answering quotes and managing proposals.
  • On internal screens, also the PO, designer and developer on the Eletro team, validating components.

Business context (inherited from the brand positioning, but relevant to UI prioritisation): the product has to read as expert, intelligent, partner and technological. It is not a consumer app, it is a data-dense working tool used by professionals throughout the workday. Which means: well-organised information density over empty minimalism, and clarity over strong visual personality.


2. Stack and scope

SystemStackHeadless base (primitives)Dark modeCharts
EDSnext 1.0 (Vue, current)Vue 3 / Nuxt UI v3 / Tailwind v4Reka UI (the Vue port of Radix UI, used internally by Nuxt UI v3)Yes, always coveredshadcn-vue chart (Unovis/vue)
EDSlegacy (Vue 2)Vue 2— (in-house library, pre Reka UI)Not implemented. Never generate, mention or validate dark mode for EDSlegacy.
EDSreact (React)React 19 / Next.js / shadcn/ui + Tailwind v4 / CVA. pnpm + Turborepo monorepo. Published package: @mercadoeletronico/eds-next-react. Repo: me-eds-next-react.Radix UI (plus @base-ui/react in some components)Yes, through next-themes (class="dark"), always coveredshadcn/ui chart (Recharts)

Careful: EDSnext (Vue) and EDSreact (React) implement the same design intent with different token naming (--ui-primary in Vue versus --primary in React, for example). That is deliberate: each stack follows the convention of its own base library (Nuxt UI v3 versus shadcn/ui). Never copy a token name from one stack to the other without checking the equivalence table in section 10.4.

EDSnext and EDSreact share the same philosophy of accessible primitives: Reka UI is the official Vue port of Radix UI, kept at API and behaviour parity (ARIA roles, focus management, keyboard navigation, portal and collision detection). In practice that means:

  • If the Nuxt UI v3 / Reka UI documentation is incomplete about an accessibility or interaction behaviour, the Radix UI documentation for the equivalent component is a reliable reference, since the API tends to mirror it.
  • EDSlegacy is the exception: it uses neither Reka UI nor Radix, it is an in-house library that predates that migration. Do not assume behaviour parity between EDSlegacy and the two current stacks.

Component naming:

  • Ticket title, Inception, Tech Guideline → full prefix (MeSidebar, MeWidget, MeTable).
  • PDD or material for a non-technical audience → no prefix (Sidebar, Widget, Table).

Public component documentation lives at eletro.design and ui.nuxt.com.

Storybook is not a documentation channel for EDS. Never document a component in it, never link to it, never propose it as the output of a task. To be precise about it: the React monorepo does contain a packages/storybook package (@mercadoeletronico/storybook, private, with dev and build scripts), but without a single story and without a mention in the repo's own AGENTS.md. It is idle scaffolding. Finding that package is not a licence to treat it as a convention or to write stories, and the Vue repo does not even have the scaffolding.

Charts: both stacks follow the shadcn chart pattern (copy-paste components with ChartContainer, ChartConfig and the --chart-1 to --chart-N tokens). In React, pnpm dlx shadcn@latest add chart (backed by Recharts). In Vue, the same components through shadcn-vue (backed by Unovis/vue). Never install another charting library (Chart.js, ApexCharts, Nivo, Highcharts and so on) and never build a chart from scratch without going through the shadcn / shadcn-vue CLI.


3. Visual Theme

The principles below apply to any stack (Vue or React). The token names quoted as examples (--ui-*) use the Vue naming. For the React equivalent, see the table in section 10.4.

  • Overall tone: utilitarian, dense, dependable. This is not a consumer brand, it is a B2B tool.
  • Primary color leads, but stays contained: the brand blue (--ui-primary in Vue, --primary in React) shows up in primary actions, links, focus and active navigation states. Do not paint large background blocks with the primary color as if it were a marketing piece (that is a brand book rule, not a product rule).
  • One verified exception: the Header bar. Header.vue applies background: var(--ui-primary) on desktop when brand.background is not provided, and background: var(--ui-bg) on mobile. It is the only product surface that fills a large block with the primary color. Any other flat blue area is wrong. brand.background exists as a white label escape hatch (a primaryColor / secondaryColor gradient and images), controlled by the consuming app, not by the DS.
  • Neutral background: --ui-bg / --background as the base of the screen, --ui-bg-elevated / --card for cards and panels, --ui-bg-muted / --muted for a light hover.
  • Hierarchy through weight and text color, not through exaggerated size: a highlighted title, standard body, metadata in secondary text, placeholder and disabled in the dimmest tone available.
  • Small to medium radius, never a pill on content elements (pill is only for badge, avatar and tag).
  • Discreet shadow. No heavy Material Design style shadows (dramatic elevation). Full scale and rules in section 7.1.
  • Motion: scale documented in section 3.1. In short, 150ms (the Tailwind default) for state feedback, 200ms for movement, ease-out entering and ease-in leaving. Never write an arbitrary duration or easing.

3.1 Motion

Taken from the compiled component themes (packages/ui/.nuxt/ui/*.ts) and from the keyframes.css in @nuxt/ui. This is not an invented scale: it is what the library components already do, now named so it can be followed on purpose instead of by accident.

CategoryDurationEasingWhere it applies
State feedback150ms (default, no class needed)cubic-bezier(0.4, 0, 0.2, 1) (default)Color change: hover, active, focus, input border. transition-colors is enough, Tailwind already resolves both values
Movement200ms (duration-200)ease-outTransform, width, translate, rotate: the chevron rotating on open, sidebar width, the tabs indicator, the switch thumb, the progress bar, an image scaling on hover
Overlay entering200msease-outanimate-[fade-in_200ms_ease-out] on modal, popover, dropdown
Overlay leaving200msease-inanimate-[fade-out_200ms_ease-in]. It leaves with the easing inverted relative to entering, on purpose
Expand and collapse200msease-out both waysanimate-[collapsible-down_200ms_ease-out] and collapsible-up, same pair for the accordion
Loadingcontinuouslinearanimate-spin (Spinner and the button loading icon)
Indeterminate and skeletoncontinuousanimate-pulse, always behind motion-reduce: when it replaces real movement
Toast restacking100msThe only use of duration-100 in the package. Do not reuse that value for anything else

Rules of use:

  • Do not write a duration outside that table. duration-[220ms], duration-300, duration-75 and any arbitrary easing are out. If an interaction does not fit any category, that is a sign the interaction needs discussing, not the duration.
  • State feedback takes no duration-*. The 150ms default is the correct value, and writing duration-150 only adds noise.
  • Direction matters. Entering ease-out, leaving ease-in. Expand and collapse is the exception, ease-out both ways, because the content is pushing layout rather than floating above it.
  • Respect prefers-reduced-motion. The package already uses motion-reduce: on continuous animation (indeterminate progress, for example). Do the same in your own animation.
  • Charts and data transitions have no defined standard. That gap is still open: shadcn-vue / Unovis brings the library's own default animation, and EDS has not decided whether to override it. Use the default and flag the gap.

4. Absolute generation rules — Vue (EDSnext / EDSlegacy)

For React (EDSreact), the equivalent rules are in section 10 (10.2 to 10.7). The underlying principle is the same, never a raw value and always a token, only the syntax changes.

  • Never use HEX, RGB, HSL or any raw color value.
  • Never use fixed numeric values for spacing, radius, font-size, font-weight, line-height or breakpoint.
  • Never use inline style="..." with a hard-coded value.
  • Exception: SVG, logo, illustration and artwork do not follow the token rule. Inside an SVG, a drawn icon, an illustration or any artwork, a hardcoded color value is the correct choice, not a violation. Two reasons: there is no token for every fill in an illustration, and, more importantly, artwork that depicts the interface must not follow the theme. The canonical case is the Header theme selector (HeaderModeSelector.vue), which draws a thumbnail of the screen with bg-[#f8fafc], bg-[#0f172a], bg-[#e2e8f0] and bg-[#1e293b]: the light theme swatch has to stay light while the user is in dark, so a semantic token would break the very thing the swatch exists to show. Same logic for the logo SVG, which section 12.1 tells you to copy literally with the fill hardcoded. What the exception does not cover: an actual UI surface (background, border, text, the hover state of an interactive element) is still a token, even when the value is written inside an arbitrary-value class.
  • Always use var(--ui-*) for semantic color.
  • For spacing, radius, typography, shadow and breakpoint, use the Tailwind v4 utility classes (gap-2, p-4, rounded-lg, text-sm, font-medium, shadow-xs, md:, lg:). If you need the raw CSS variable, the name is Tailwind's own: --spacing, --radius-*, --text-*, --font-weight-*, --shadow-*, --breakpoint-*. Values in sections 6, 7 and 8.
  • --eds-* does not exist. No file in the package defines that namespace: verified by grep across packages/ui/app/assets/*.css, the bundle (packages/ui/.nuxt/ui.css) and dist/. The only occurrences in the repo are this file and skills/eds-next-tokens/references/tokens-full.md. Generating var(--eds-spacing-4) produces CSS that resolves to nothing. If you find --eds-* in code, it is a bug, not a convention.
  • Always express spacing, radius and typography in rem, never in px. Reminder: 1rem = 16px. The Tailwind classes are already rem, so using the classes is how you comply.
  • Never confuse the institutional brand palette (marketing, brand book, presentations) with the product color tokens (--ui-*). They serve different purposes and hold different values (see section 12).
  • Always cover dark mode when a background or text color is used, except on EDSlegacy, where dark mode does not exist and must not be suggested.
  • Always guarantee visible focus (:focus-visible with an outline based on --ui-primary).
  • Always guarantee AA contrast (--ui-text on --ui-bg, --ui-text-inverted on --ui-primary).
  • Always guarantee responsiveness with the standard Tailwind breakpoints (sm to 2xl, see section 6.1). Never define a custom breakpoint.
  • New components are always TypeScript.
  • Brazilian Portuguese for all product copy, unless another language is explicitly requested.
  • Always use Lucide as the product's standard icon library. No other icon library may be used. Source of truth for the icon name: https://lucide.dev/icons/, the official gallery. The name on the site is kebab-case (file-input, messages-square) and it is exactly what follows the prefix in code: i-lucide-file-input (Vue, through UIcon), lucide:file-input (Vue, in item config) or <FileInput /> from lucide-react (React, the same name in PascalCase). If the icon does not exist at https://lucide.dev/icons/, it does not exist: do not invent an alias and do not use a name from another library.
  • Always use the shadcn / shadcn-vue chart component for any chart (Recharts in React, Unovis/vue in Vue). Never install another charting library.
  • Always check whether the component already exists in the package (@mercadoeletronico/eds-next-react or Nuxt UI v3 / EDSnext) before creating a new one. Never reimplement from scratch a component that already exists in the library.
  • Never render the ME logo as typeset text ("me.", "ME" and so on). Always use the real SVG (see section 12). The package does not export the logo as a component yet, so the SVG code has to be copied literally.

5. Color Tokens — Vue (semantic color)

5.1 Where the tokens come from (confirmed in the code, not in the MCP)

Careful: the eletrods MCP serves a hand-written markdown file (docs/server/mcp/references/tokens.md), not introspection of the real CSS. It can drift from the code without breaking anything, and it has drifted before. Do not blindly trust the MCP or the docs site for an exact hex, check the repo when you need precision.

The --ui-* tokens come from three different origins, and none of them is a tokens.css file:

a) The neutral scale and text / background / border, hardcoded in @nuxt/ui v4.10.0 (dist/runtime/index.css, also compiled into packages/ui/dist/style.css):

TokenLightDark
--ui-text-dimmed--ui-color-neutral-400--ui-color-neutral-500
--ui-text-muted--ui-color-neutral-500--ui-color-neutral-400
--ui-text-toned--ui-color-neutral-600--ui-color-neutral-300
--ui-text--ui-color-neutral-700--ui-color-neutral-200
--ui-text-highlighted--ui-color-neutral-900#fff
--ui-text-inverted#fff--ui-color-neutral-900
--ui-bg#fff--ui-color-neutral-900
--ui-bg-muted--ui-color-neutral-50--ui-color-neutral-800
--ui-bg-elevated--ui-color-neutral-100--ui-color-neutral-800
--ui-bg-accented--ui-color-neutral-200--ui-color-neutral-700
--ui-bg-inverted--ui-color-neutral-900#fff
--ui-border--ui-color-neutral-200--ui-color-neutral-800
--ui-border-muted--ui-color-neutral-200--ui-color-neutral-700
--ui-border-accented--ui-color-neutral-300--ui-color-neutral-700
--ui-border-inverted--ui-color-neutral-900#fff

Non-semantic, same file: --ui-radius: 0.25rem, --ui-header-height: 4rem, --ui-container: 80rem.

b) The --ui-color-{alias}-{50..950} families and the 6 --ui-{alias} aliases live in no file at all, they are generated at runtime by a Vue plugin (@nuxt/ui/dist/runtime/plugins/colors.js) from appConfig.ui.colors. The pattern is --ui-color-{key}-{shade}: var(--color-{tailwindAlias}-{shade}, <Tailwind default hex>), and --ui-{key} points at stop 500 (light) or 400 (dark) of that scale.

Aliases as resolved today. Source of truth: packages/ui/app/app.config.ts, which contains exactly ui: { colors: { primary: 'blue' } }. Every other alias comes from the Nuxt UI v4 default. Do not use packages/ui/.nuxt/app.config.mjs to check: it is a generated artifact and can be stale (in the local copy from 2026-08-10 it still says primary: "green", the Nuxt UI default, because it was generated before the override).

Consequence for a consuming app: the package's app.config.ts applies inside the DS playground itself. An app installing @mercadoeletronico/eds-next has to declare primary: 'blue' in its own app.config.ts, otherwise Nuxt UI resolves --ui-primary to green. That is not optional.

AliasTailwind scale
primaryblue (customised, see below)
secondaryblue
successgreen (Tailwind default)
infoblue
warningyellow (Tailwind default)
errorred (Tailwind default)
neutralslate (Tailwind default, with no --ui-neutral alias, only the --ui-color-neutral-* scale)

Which yields, actually existing at runtime: --ui-color-{alias}-{50,100,...,950} (7 families × 11 stops) plus --ui-primary, --ui-secondary, --ui-success, --ui-info, --ui-warning and --ui-error.

Hover and active use no token of their own, they use an opacity modifier. There is no --ui-primary-strong, nor anything ending in -strong, and that is not a gap: it is the library's convention. Verified in the compiled UButton theme (packages/ui/.nuxt/ui/button.ts), where hover and active apply opacity over the semantic color itself, with the same value in both states:

VariantHover and active
solidhover:bg-{color}/75 and active:bg-{color}/75
softhover:bg-{color}/15 and active:bg-{color}/15
ghost and linkhover:bg-{color}/10 and active:bg-{color}/10
Neutral surfacehover:bg-accented/75, hover:bg-elevated, hover:bg-inverted/90
Table rowhover:bg-elevated/50
Fixed-color surface (the blue Header bar)hover:bg-black/10. --color-black is a Tailwind token, so the class is not an arbitrary value. In the Header code this currently appears as hover:bg-[rgba(0,0,0,0.1)], the same result written in arbitrary form: prefer bg-black/10 in new code. Do not use --ui-bg-inverted here, because it inverts with the theme and the bar does not

where {color} is one of the aliases from section 5.1. Non-interactive components (Badge, Alert, Card) have no hover at all in the theme, so do not invent one.

Do not use an adjacent stop of the scale for hover (-600 instead of -500). That was this file's earlier recommendation and it was wrong: it diverged from what the components do, and in dark mode it moved to the wrong side of the scale. An opacity modifier behaves the same in both themes, which is precisely why the library does it that way.

In React the intent is the same with slightly different numbers, verified in button.tsx: hover:bg-primary/80 on solid, hover:bg-destructive/20 and /30 on destructive, hover:bg-muted and hover:bg-muted/50 on a neutral surface. A minor divergence to settle some day: Vue uses /75 and React uses /80 for the same state. Until there is a decision, follow the number of the stack you are in.

c) The brand hexes: only the blue scale is customised, in packages/ui/app/assets/primary.css (--color-blue-50 to --color-blue-950). It is the only file in the package that defines a brand color; green, red, yellow and slate stay at the Tailwind 4 default.

StopHexUse
blue-50#EDF3FF
blue-100#D0E0FF
blue-200#A9C6FF
blue-300#7CA8FF
blue-400#437FFF--ui-primary in dark
blue-500#1052E0--ui-primary in light
blue-600#0C3DA8
blue-700#082970
blue-800#061D4E
blue-900#041438
blue-950#020C22

A known divergence between the docs site and the package: the site (eletro.design) overrides green, red, yellow and blue-600 with its own values, different from the published package (docs/app/assets/css/main.css). The published package is the absolute source of truth for generating code, since it is what a consuming app actually installs and renders. The docs site is misaligned on those tokens and should not be used to confirm an exact hex until it is fixed.

5.2 Real values of the neutral scale and the state colors

Verified in node_modules/tailwindcss/theme.css (Tailwind v4.3.3, what the repo resolves today). They are oklch, not hex. Do not convert them to hex when generating code, use the token name; the list below exists for anyone who has to render outside Tailwind (a plain HTML prototype, an email, Figma).

--ui-color-neutral-* resolves to --color-slate-*:

StopReal value
slate-50oklch(98.4% 0.003 247.858)
slate-100oklch(96.8% 0.007 247.896)
slate-200oklch(92.9% 0.013 255.508)
slate-300oklch(86.9% 0.022 252.894)
slate-400oklch(70.4% 0.04 256.788)
slate-500oklch(55.4% 0.046 257.417)
slate-600oklch(44.6% 0.043 257.281)
slate-700oklch(37.2% 0.044 257.287)
slate-800oklch(27.9% 0.041 260.031)
slate-900oklch(20.8% 0.042 265.755)
slate-950oklch(12.9% 0.042 264.695)

State colors, at the two stops the aliases use (500 in light, 400 in dark):

AliasScaleStop 500 (light)Stop 400 (dark)
--ui-successgreenoklch(72.3% 0.219 149.579)oklch(79.2% 0.209 151.711)
--ui-warningyellowoklch(79.5% 0.184 86.047)oklch(85.2% 0.199 91.936)
--ui-errorredoklch(63.7% 0.237 25.331)oklch(70.4% 0.191 22.216)
--ui-primaryblue (custom)#1052E0#437FFF
--ui-secondary, --ui-infoblue (custom)#1052E0#437FFF

Blue is the only one still expressed in hex, because it comes from the package's own primary.css (section 5.1c) and not from Tailwind. Confirmed in the file: besides the 11 stops, primary.css also declares --font-sans: "Roboto", sans-serif (this is where the product font enters, see section 8).

5.3 Dark mode in Vue: the real mechanism

It is not plain prefers-color-scheme, and it is not the next-themes from section 10.7. It is the package's own useTheme() composable (packages/ui/app/composables/useTheme/useTheme.ts), built on VueUse's useColorMode:

  • Three modes: 'light' | 'dark' | 'auto', default auto.
  • Persisted in localStorage under the key nuxt-color-mode (with a read fallback to vueuse-color-scheme, so a preference from an older app is not lost).
  • Effect on the DOM, on <html>: it adds or removes the dark class and writes data-theme="light|dark". In auto mode the data-theme attribute is removed and only the dark class follows the system.
  • Nuxt UI matches this through @variant dark (&:where(.dark, .dark *)) and @variant light (&:where(.light, .light *)), declared in @nuxt/ui/dist/runtime/index.css.

In other words: the dark mode selector is the same in both stacks (a .dark class on an ancestor), only the controller differs (useTheme in Vue, next-themes in React). When writing your own CSS, express the light and dark pair as :root (or .light) and .dark, never inside a media query.

The theme switching UI already exists in the Header avatar menu (HeaderAvatar.vue), with the lucide:moon icon in dark and lucide:sun in light. Do not build a new toggle.

6. Spacing — Vue (multiples of 0.25rem / 4px)

Default unit: rem, not px. Remember: 1rem = 16px (the browser's base font size). The px values below are visual reference only.

The real mechanism: Tailwind v4 defines a single base variable, --spacing: 0.25rem, and derives everything else by multiplication. Neither --spacing-4 nor --eds-spacing-4 exists. You consume it through the class (p-4, gap-2, size-6, mt-8), and the class expands to calc(var(--spacing) * n). In your own CSS, the supported form is Tailwind's --spacing(n) function.

ClassResolved valuepx equivalentWhen to use
gap-1, p-10.25rem4pxgap between icon and label
gap-2, p-20.5rem8pxcompact padding
gap-3, p-30.75rem12pxchip and tag padding, table cell padding-x
gap-4, p-41rem16pxdefault button padding
gap-6, p-61.5rem24pxcard padding
gap-8, p-82rem32pxgap between sections
gap-12, p-123rem48pxcontainer spacing

Fractional steps (p-2.5, gap-1.5) are valid and do appear in the package code. Use them sparingly, and only when density demands it.

6.1 Breakpoints

The standard Tailwind scale (identical in both stacks), and never define a custom breakpoint. The real values are in rem, not px: --breakpoint-sm to --breakpoint-2xl in tailwindcss/theme.css.

PrefixTokenReal valuepx equivalentTypical use
sm:--breakpoint-sm40rem640pxlight adjustment, still mobile
md:--breakpoint-md48rem768pxtablet, columns start splitting
lg:--breakpoint-lg64rem1024pxstandard desktop, sidebar visible
xl:--breakpoint-xl80rem1280pxlarge desktop, more columns
2xl:--breakpoint-2xl96rem1536pxwide screens

Container queries are also a package convention, not just viewport breakpoints. Subheader.vue and Forehead.vue use @container with @min-[700px]: and @2xl: to adapt to the space of the slot rather than the window. In a block that lives inside a layout with a sidebar, a container query is the correct choice.

7. Radius — Vue

Default unit: rem (except rounded-full, which uses a fixed px value by convention, since it stands for "infinite").

The real names are Tailwind v4's (--radius-*), and the scale has 8 steps, not 4. The four values this file used to document were right, only the prefix was wrong.

ClassTokenValuepx equivalentUse
rounded-xs--radius-xs0.125rem2pxminimal detail, rare
rounded-sm--radius-sm0.25rem4pxdefault input
rounded-md--radius-md0.375rem6pxdefault button
rounded-lg--radius-lg0.5rem8pxcard
rounded-xl--radius-xl0.75rem12pxmodal
rounded-2xl--radius-2xl1rem16pxlarge overlay
rounded-3xl--radius-3xl1.5rem24pxrare in product
rounded-4xl--radius-4xl2rem32pxrare in product
rounded-full(fixed)9999px9999pxavatar, pill badge

Nuxt UI keeps its own --ui-radius: 0.25rem separately, which is the base radius of the library components (section 5.1a). Do not confuse the two: --ui-radius is the components' internal default, --radius-* is the scale you apply in your own layout.

7.1 Elevation (Shadow)

The base color is always black with opacity, never a colored shadow. Prefer the low levels (2xs to sm) day to day; lg, xl and 2xl are for overlays (modal, popover, floating dropdown). A single rule for both Vue and React: never use an invented or arbitrary shadow value (neither raw CSS nor Tailwind's shadow-[0_2px_4px_...]), always one of these tokens or classes.

The official scale is Tailwind v4's (--shadow-*), consumed through the shadow-2xs to shadow-2xl classes. There is no --eds-shadow-*.

ClassTokenReal CSS (Tailwind v4.3.3)Use
shadow-2xs--shadow-2xs0 1px rgb(0 0 0 / 0.05)Subtle bottom border, a divider with minimal depth
shadow-xs--shadow-xs0 1px 2px 0 rgb(0 0 0 / 0.05)Input, card at rest, the Header bar
shadow-sm--shadow-sm0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)Elevated card, button on hover
shadow-md--shadow-md0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)Dropdown, context menu
shadow-lg--shadow-lg0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)Popover, floating tooltip
shadow-xl--shadow-xl0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)Modal, dialog
shadow-2xl--shadow-2xl0 25px 50px -12px rgb(0 0 0 / 0.25)Maximum emphasis overlay (rare, an onboarding spotlight for example)

The package does not override --shadow-*, on purpose. Two practical consequences:

  • When generating or reviewing code: use the Tailwind class. The code is correct by definition.
  • The md, lg and xl effects in the Figma library do not follow this scale yet. A Figma screen with a shadow at those three levels will look slightly different from the implementation, and in that case the implementation is the reference, not Figma.

7.2 Border width and icon size

Two gaps that surfaced while generating code outside Tailwind, now closed:

  • Border width has no token and does not need one. Tailwind v4 exposes no --border-width-* namespace (verified in theme.css). Use the classes: border (1px), border-2, border-t, border-b. The "never a fixed px" rule from section 4 applies to a value written by hand in CSS, not to these classes. For the border color, use the Nuxt UI semantic utilities: border-default (which maps to --ui-border) and border-accented (which maps to --ui-border-accented), the two actually used in the package.
  • Icon size comes from the spacing scale. Use size-*, which resolves to calc(var(--spacing) * n). The convention observed in the package code: size-4 (1rem) for an icon inside text or a small button, size-5 (1.25rem) for a standard action, size-6 (1.5rem) in the Header chrome (menu, search and cart on mobile).

8. Product typography

Important: the product uses Roboto, not Montserrat, in both stacks (Vue and React). Montserrat is exclusive to marketing and institutional material. That is an active, deliberate difference between the two systems, not an oversight.

The token names below use the Vue naming. In React the family arrives through --font-sans / --font-heading injected by the consuming app, see section 10.6.

Family. The real token is --font-sans, declared in packages/ui/app/assets/primary.css inside @theme static as "Roboto", sans-serif. That is the token Tailwind uses as --default-font-family, so font-sans and body already inherit Roboto with no work from you. There is no --eds-font-family. Just make sure the consuming app loads the Roboto webfont.

Sizes. The real names are --text-*, consumed through text-xs to text-3xl. Each size brings its paired line-height in --text-{size}--line-height, and in v4 that value is a unitless ratio (calc(1.25 / 0.875)), not a rem. Using the text-sm class already applies the size and line-height pair, which is what "always paired" means in practice.

Class--text-*Paired line-heightRendered
text-xs0.75remcalc(1 / 0.75)12px / 16px
text-sm0.875remcalc(1.25 / 0.875)14px / 20px
text-base1remcalc(1.5 / 1)16px / 24px
text-lg1.125remcalc(1.75 / 1.125)18px / 28px
text-xl1.25remcalc(1.75 / 1.25)20px / 28px
text-2xl1.5remcalc(2 / 1.5)24px / 32px
text-3xl1.875remcalc(2.25 / 1.875)30px / 36px

Weights. The real names are --font-weight-*. Tailwind exposes nine steps (thin 100 to black 900), but in product use only four: font-normal (400) for body, font-medium (500) for labels, buttons and table headers, font-semibold (600) for page and section titles, font-bold (700) for a highlighted number (a KPI). Weights 100, 200, 300, 800 and 900 exist in Tailwind and have no use in product.

9. Semantic map per component — Vue

Read this before the table. This map serves two cases: reviewing whether a component uses the right color, and rendering outside Nuxt UI (a plain HTML prototype, an email, Figma). Inside Vue, a component that exists in the library is configured through props, not through a hand-written token. Writing background: var(--ui-primary) on a <UButton> is a violation, not an implementation.

ComponentBackgroundTextBorderRadius
Button primary--ui-primary--ui-text-invertedrounded-md
Button secondary--ui-bg--ui-primary--ui-primaryrounded-md
Button ghosttransparent--ui-textrounded-md
Card--ui-bg-elevated--ui-text--ui-borderrounded-lg
Input--ui-bg--ui-text--ui-borderrounded-sm
Input --ui-bg--ui-text--ui-primaryrounded-sm
Badge (neutral example)--ui-bg-accented--ui-text-mutedrounded-full
Header (desktop)--ui-primarywhite (text-white)brand.iconColor when provided— (full width bar, shadow-xs, height --ui-header-height)
Header (mobile)--ui-bg--ui-text
Subheaderinherits --ui-bg (it is a UDashboardToolbar)--ui-textthe toolbar border
Table (MeTable)its own namespace, see below

Badge and Alert follow the same rule: props, never a manual token. The Badge row above is only the neutral example. The real UBadge accepts 7 colors (primary, secondary, success, info, warning, error, neutral) × 4 variants (solid, outline, soft, subtle) × 5 sizes (xs to xl), verified in packages/ui/.nuxt/ui/badge.ts. A colored status is <UBadge color="success" variant="soft" />, not a hand-painted background. The same holds for UAlert (<UAlert color="success" variant="soft" /> or variant="subtle"), which resolves to bg-{color}/10 text-{color} on soft, with an extra ring on subtle.

9.1 MeTable has its own token namespace

packages/ui/app/assets/me-table.css declares the table tokens in a separate namespace, outside the --ui-* scale, with raw oklch values and heights in px. It is deliberate according to the file's own comment ("all visual tokens are owned here"), so when working on the table use these names and do not try to translate them into --ui-*:

TokenValueRole
--color-table-borderoklch(0.92 0.01 240)cell and header border
--color-table-row-hoveroklch(0.97 0.005 240)row hover
--color-table-row-selectedoklch(0.95 0.02 230)selected row
--color-table-header-bgoklch(0.98 0.005 240)header background
--color-table-mutedoklch(0.6 0.01 240)secondary cell text
--color-table-focus-ringoklch(0.7 0.18 250)focus ring
--table-row-height-compact / default / comfortable32px / 40px / 56pxthe three densities
--table-cell-padding-x / -y0.75rem / 0.5remcell padding
--table-resize-handle-width4pxcolumn resize handle
--table-highlight-*primary and secondary come from --color-blue-*; success, warning and error are local oklchrow highlight bar
--me-ctx-*its own setcontext menu (it is teleported to <body>, so it has to be global)

Dark mode overrides come in :root.dark in the same file.

Target state. The namespace stays (the table does need its own tokens for density and for the teleported context menu), but every token is meant to derive from the --ui-* scale instead of carrying hand-written oklch, and the measurements move to rem. The table below is the target, not the current state of the file:

Table tokenValue todayTarget
--color-table-borderoklch(0.92 0.01 240)var(--ui-border)
--color-table-header-bgoklch(0.98 0.005 240)var(--ui-bg-muted)
--color-table-row-hoveroklch(0.97 0.005 240)--ui-bg-elevated at 50%, which is the hover:bg-elevated/50 the Nuxt UI table theme already uses
--color-table-row-selectedoklch(0.95 0.02 230)--ui-primary at 10%, aligned with the soft pattern from section 5.1
--color-table-mutedoklch(0.6 0.01 240)var(--ui-text-muted)
--color-table-focus-ringoklch(0.7 0.18 250)var(--ui-primary)
--table-resize-handle-coloroklch(0.85 0.02 240)var(--ui-border-accented)
--table-resize-handle-color-activeoklch(0.65 0.18 250)var(--ui-primary)
--table-resize-handle-width4px0.25rem, that is var(--spacing)
--table-row-height-compact32px2rem
--table-row-height-default40px2.5rem
--table-row-height-comfortable56px3.5rem
--table-highlight-success | -warning | -errorhand-written oklchvar(--ui-success) | var(--ui-warning) | var(--ui-error)
--table-highlight-primary | -secondary | -info--color-blue-*var(--ui-primary) and stops of the primary scale
--table-highlight-neutral--color-table-mutedvar(--ui-text-muted)
--me-ctx-bg | -border | -text | -text-mutedhand-written oklch--ui-bg | --ui-border | --ui-text | --ui-text-muted
--me-ctx-item-hover-bg--color-table-row-hover--ui-bg-elevated at 50%
--me-ctx-danger-color | -warning-color | -success-colorhand-written oklch--ui-error | --ui-warning | --ui-success
--me-ctx-shadow0 12px 40px oklch(...)var(--shadow-md), the dropdown and context menu level from section 7.1
--me-ctx-kbd-bg | -kbd-borderhand-written oklch--ui-bg-muted | --ui-border

Two notes for whoever executes this:

  • px to rem is a refactor with no visual change (32px = 2rem at the base font size), so it can be done safely.
  • The colors are not pixel-identical. The current oklch values were written by hand and land close to, not equal to, the semantic equivalents. The migration shifts the border, hover and header colors by a few percentage points and needs visual validation, it is not a mechanical swap. The gain: the table starts following the theme and any palette adjustment like the rest of the product, instead of needing parallel maintenance.

9.2 Header, Subheader and Forehead are three different things

A common generation mistake is treating "subheader" as a bar with breadcrumb, title and actions. That is not what the package delivers.

  • Header (app/blocks/Header/): the global bar, h-16, z-[10000], shadow-xs, white text. The full layout contract is in 9.3, and it is what fixes the alignment mistakes that show up in generated screens. Composed of HeaderBrand (logo), HeaderNavigation (navigation items), HeaderAvatar (profile menu, theme selector and language submenu) and HeaderActions (used when no user is logged in). On mobile it switches to HeaderAvatarMobile and reveals the menu, search and cart icons. Main props: user, brand, navigationItems, profileItems, siteMapItems, actionItems, hideOptions, cartBadge, disableModeSelector. It renders a 64px USkeleton while there is neither user.name nor actionItems.
  • Subheader (app/blocks/Subheader/): a screen toolbar, not an editorial header. It wraps UDashboardToolbar and has three regions: the left slot (default: the sidebar toggle button with lucide:menu and the create group with lucide:plus plus a lucide:chevron-down dropdown), the default slot in the centre (default: MeButtonBar) and the right slot (default: search with lucide:search and lucide:list-filter, the chart toggle with lucide:chart-column, and the grid mode selector). Props: variant (primary | secondary | tertiary), toggleSidebar, createActions, buttonBar, search, toggleChart, gridMode, back, confirm. The slot defaults only appear on variant="primary" and above 700px of container width.
  • Forehead (app/components/Forehead/): a record or document header, not a screen header. Structure: an optional MeForeheadBadge on the left (banner variants by icon, image, user or text), a leading slot and a trailing slot (which aligns right from @2xl up), plus a drawer slot that opens below when openDrawer is set. Props: bannerVariant, textBanner, imageBanner, userBanner, iconBanner, dropdownItems, openDrawer. The screen title and metadata go in through the slots, the component has no title prop.

9.3 Header anatomy: the layout contract

Taken straight from the components (app/blocks/Header/). This is the section to follow when the request is "build a screen with the ME header": generation tools get the color right and the structure wrong, always at the same points.

The bar has exactly two groups, and that is what defines the alignment. Header.vue uses <nav class="flex items-center justify-between h-16"> with two children and nothing else:

┌───────────────────────────────────────────────────────────────────────────────────────┐
│ [logo]                          [Dashboard] │ [Transações][Catálogos][Usuários][Mais] │
│                                             │ [Mensagens][Genius] [avatar]            │
└───────────────────────────────────────────────────────────────────────────────────────┘
  └─ group 1: the logo only                     └─ group 2: navigation + avatar, all of
     (on mobile, the hamburger menu first)          it pushed right by ml-auto
ElementRule verified in the code
Barh-16 (which is --ui-header-height), z-[10000], shadow-xs, text-white. Background var(--ui-primary) on desktop and var(--ui-bg) on mobile
Group 1 (left)The brand only. HeaderBrand is an <a> with flex items-center justify-center px-4 shrink-0 and the image at h-8 w-auto max-w-[120px] (h-5 on mobile, using brand.logoMobile when present)
Group 2 (right)HeaderNavigation carries ml-auto on its root (ml-auto flex justify-center h-full items-stretch), so navigation and avatar sit flush right. Never place the menu items right after the logo
Navigation itemStacked, not inline: grid place-items-center gap-1 px-[12px] py-2, with the icon (size-5) on top and the label below (text-xs font-medium leading-4 whitespace-nowrap). Icon and label both take the color from brand.iconColor
Active stateA 4px bar at the bottom edge of the item, through ::after with h-1 w-full rounded-t-full bottom-0 left-0, in the iconColor. It is not a filled pill, not a darker background, not underlined text
Item hoverbg-black/10 with transition-colors duration-200
Item badgeAnchored to the icon, absolute -top-1 -right-3, color="error", size="sm", min-w-5, pill. It disappears when the value is empty or "0"
SeparatorA vertical rule of 1px by 40px, opacity-35, mx-[12px], self-center, in the iconColor. The default set has two: after Dashboard and before Mensagens
Avatar (desktop)The avatar only. The trigger is pl-6 pr-4 h-full flex items-center with a UAvatar size="xl" showing user.acronym, root bg-black/30 and a transparent border that becomes border-white border-2 on group hover. A UChip at bottom-right when user.badge is present
User nameIt does not appear in the bar. Name, role and email live inside the dropdown, in the #avatar-header slot, through UUser (name plus description = role or email). The dropdown opens with align: 'end' and width w-62
MobileThe menu (lucide:menu) sits at the far left, before the logo. On the right, search (lucide:search) and cart (lucide:shopping-cart, with a counting UBadge), and HeaderAvatarMobile in place of the desktop avatar
No userIf there is neither user.name nor actionItems, the component renders a 64px tall USkeleton, not an empty bar

Recurring generation mistakes, with the correct form beside them:

MistakeCorrect
Menu items aligned left, right after the logoAligned right, in the same group as the avatar (ml-auto)
Icon beside the label in a menu itemIcon on top, label below, both centred
Active item as a filled pill or a darker backgroundA 4px bar at the bottom of the item
Avatar accompanied by the user name and a chevron in the barThe avatar only, with the initials. Name and role only inside the dropdown
Mensagens and Genius as loose icons without a labelThey are ordinary navigation items, with icon and label
No separatorsTwo vertical rules at 35% opacity delimiting the three navigation blocks

10. React stack (@mercadoeletronico/eds-next-react)

Based on the me-eds-next-react repo (branch develop), package @mercadoeletronico/eds-next-react 1.1.1. Check the AGENTS.md at the root of the monorepo before assuming anything here is still true.

10.1 Repository architecture

A pnpm + Turborepo monorepo:

me-eds-next-react/
├── apps/
│   └── web/                      # Next.js playground (private, not published)
│       ├── app/                  # App Router
│       └── components/           # App-level components (ThemeProvider, for example)
├── build/                        # Dockerfile, Jenkinsfile, CI scripts
└── packages/
    ├── ui/                       # @mercadoeletronico/eds-next-react (published package)
    │   └── src/
    │       ├── components/       # One file per component, kebab-case (button.tsx, alert-dialog.tsx)
    │       ├── hooks/             # Shared hooks (use-mobile.ts)
    │       ├── lib/               # utils.ts (the cn helper)
    │       └── styles/            # globals.css (tokens + base styles)
    ├── storybook/                # @mercadoeletronico/storybook, private and EMPTY (zero stories, see section 2)
    ├── eslint-config/
    └── typescript-config/

Two things the repo's AGENTS.md does not list yet but that do exist in the tree: packages/storybook (scaffolding without a story, see section 2) and build/ (Dockerfile, Jenkinsfile and CI scripts). The repo's AGENTS.md is the reference for conventions, not for a folder inventory.

Built on shadcn/ui (components.json with "style": "radix-nova", baseColor: "neutral", cssVariables: true) plus Radix UI (radix-ui, @base-ui/react) as primitives. Icons: Lucide through the lucide-react package, the product's mandatory standard icon library, no other (Heroicons, Font Awesome, Material Icons). Check the icon name at https://lucide.dev/icons/ and import it in PascalCase: the site's file-input is import { FileInput } from "lucide-react". The repo's components.json already declares iconLibrary: "lucide".

10.2 How to style (different from Vue)

In Vue (EDSnext) the convention is a Tailwind utility class in the template, with var(--ui-*) in <style scoped> when a semantic color has to enter your own CSS (--eds-* does not exist, see section 4). In React the difference is not that, it is the absence of <style scoped>. The repo's real convention is:

  • Tailwind v4 utilities directly in className, no CSS Modules, no styled-components, no <style>.
  • Class composition always through the cn() helper:
    import { cn } from "@mercadoeletronico/eds-next-react/lib/utils"
    // cn() = clsx(...) + twMerge(...) — never concatenate a class string by hand
    
  • Component variants always through class-variance-authority (cva), never if/else on className scattered through the JSX.

A real template of a component with variants (button.tsx, abridged):

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"

import { cn } from "@mercadoeletronico/eds-next-react/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center rounded-lg text-sm font-medium transition-all outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline: "border-border bg-background hover:bg-muted",
        secondary: "bg-secondary text-secondary-foreground",
        ghost: "hover:bg-muted hover:text-foreground",
        destructive: "bg-destructive/10 text-destructive hover:bg-destructive/20",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: { default: "h-8 px-2.5", sm: "h-7 px-2.5 text-[0.8rem]", lg: "h-9 px-2.5" },
    },
    defaultVariants: { variant: "default", size: "default" },
  }
)

function Button({
  className,
  variant = "default",
  size = "default",
  asChild = false,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot.Root : "button"
  return (
    <Comp
      data-slot="button"
      data-variant={variant}
      data-size={size}
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  )
}

export { Button, buttonVariants }

Mandatory patterns in every new component:

  • A named export (never export default).
  • data-slot="component-name" always present (it is the hook used by other components and by composition CSS, has-data-[slot=card-footer] for example).
  • data-variant and data-size mirroring the props, when the component has variants.
  • Typing through React.ComponentProps<"tag"> (the element's native props) & VariantProps<typeof xVariants> (the cva variant props).
  • Support for polymorphic composition (asChild plus Slot.Root from the radix-ui package) when the component can wrap another element (Button, Badge and so on).
  • File in kebab-case (button-group.tsx), function and export in PascalCase (ButtonGroup).

10.3 Imports — a non-negotiable rule

Never import with a relative path that crosses the package (../../lib/utils). Always through the package subpath:

import { cn } from "@mercadoeletronico/eds-next-react/lib/utils"
import { Button } from "@mercadoeletronico/eds-next-react/components/button"

This is checked in the repo's own PR checklist (AGENTS.md): a relative import breaks the exports map in package.json when the package is consumed from outside.

10.4 Tokens — shadcn naming (different from Vue) and the equivalence table

The tokens live in packages/ui/src/styles/globals.css, defined as CSS custom properties on :root and .dark, and exposed to Tailwind through @theme inline. The naming is shadcn/ui's, not the Nuxt UI v3 naming used in Vue:

Design intentVue token (EDSnext)React token (eds-next-react)Tailwind class
Brand primary color--ui-primary--primarybg-primary / text-primary
Text on a primary background--ui-text-inverted--primary-foregroundtext-primary-foreground
Secondary action--ui-secondary--secondary / --secondary-foregroundbg-secondary
Error / destructive--ui-error--destructivetext-destructive / bg-destructive/10
Default text--ui-text--foregroundtext-foreground
Secondary text, metadata--ui-text-muted--muted-foregroundtext-muted-foreground
Page background--ui-bg--backgroundbg-background
Elevated card background--ui-bg-elevated--card / --card-foregroundbg-card
Default border--ui-border--borderborder-border (or plain border, already mapped)
Border and focus ring--ui-border-accented--ringfocus-visible:ring-ring/50

Real values of the brand blue palette (identical in intent to Vue): the --color-blue-50 to --color-blue-950 scale. Light primary = blue-500 (#1052E0), dark primary = blue-400 (#437FFF), the same values the --ui-primary token already documents in Vue, only exposed as a palette scale (--color-blue-*) instead of a single semantic token. The full table with all 11 stops is in 5.1.

Known gap, do not invent: globals.css has no --success, --warning or --info (Vue has --ui-success, --ui-warning and --ui-info). Of the 32 tokens on :root, only --destructive exists for state. This is the only token gap left, and closing it depends on deciding the values with the team, not on reading code. If a React component needs a success or warning state, do not create a new value on your own: use the closest semantically available one (chart-*, or primary for a neutral info) and flag the gap to the Eletro team instead of pinning a new hex inside the component.

10.5 Radius and spacing (its own scale, do not copy from Vue)

Radius is derived from a single base variable (--radius: 0.625rem) through calc(), with a different numeric scale from Vue's. Do not assume 1:1 parity:

React tokenFormulaApproximate pxTailwind class
--radius-smcalc(var(--radius) * 0.6)~6pxrounded-sm
--radius-mdcalc(var(--radius) * 0.8)~8pxrounded-md
--radius-lgvar(--radius)10pxrounded-lg (button and input default)
--radius-xlcalc(var(--radius) * 1.4)~14pxrounded-xl (card)
--radius-2xl to --radius-4xllarger multiples18–26pxmodals, pill badge (rounded-4xl)

Spacing has no custom scale of its own in either stack. Vue and React both use the standard Tailwind scale directly in the classes (gap-1.5, px-2.5, h-8) and, when a component needs a reusable variable, they create a local custom property per component ([--card-spacing:--spacing(4)] inside Card itself, for example). The --eds-spacing-N this file used to document exists nowhere, see section 4.

Elevation uses Tailwind's shadow-2xs to shadow-2xl utility classes, the same scale and rule as section 7.1.

10.6 Typography in React

The family (--font-sans, also mapped to --font-heading) is injected by the consuming app (through next/font in the host's Next.js), not hardcoded in the package. The business rule still holds: product means Roboto. The package itself does not force the font, it only consumes the variable, so make sure the integrating app loads Roboto and exposes it as --font-sans.

10.7 Dark mode in React

The mechanism is next-themes, not plain prefers-color-scheme.

// apps/web/components/theme-provider.tsx
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
  {children}
</NextThemesProvider>

Tailwind recognises dark mode through @custom-variant dark (&:is(.dark *)) in globals.css. In other words: dark mode depends on the .dark class on an ancestor (controlled by next-themes), not on an automatic media query by default. When generating a new component, make sure every semantic color used (bg-*, text-*, border-*) already has a counterpart on :root and .dark. If you are using an existing token from globals.css that comes for free; if you are using an arbitrary color, you have broken rule 10.4.

10.8 Adding a new component

Before anything else: confirm the component does not already exist in packages/ui/src/components/ or in the public docs (eletro.design). Only then follow the flow the repo expects (documented in AGENTS.md):

pnpm dlx shadcn@latest add <component-name> -c apps/web

That writes straight into packages/ui/src/components/. There is no need to wire anything up manually: the ./components/* wildcard export in the package's package.json covers any new file automatically.

Checklist before considering it done (from AGENTS.md itself):

  1. Component created through the shadcn CLI or manually in packages/ui/src/components/.
  2. Uses @mercadoeletronico/eds-next-react/* imports, never relative ones.
  3. pnpm typecheck passes.
  4. pnpm lint passes.
  5. A usage example added in apps/web for visual validation.
  6. pnpm dev confirms it renders correctly.

10.9 Commit, branch and PR conventions (React)

  • Commit: Jira MW-XXXXX #time Xh - description (not conventional commits).
  • Branch: feature/MW-XXXXX and fix/MW-XXXXX from develop; hotfix/MW-XXXXX and hotfeature/MW-XXXXX from master.
  • MR: squash and merge. The PR title includes the Jira code.

10.10 Consuming app setup (Next.js host)

When another team consumes the package:

// next.config.ts
const nextConfig = { transpilePackages: ["@mercadoeletronico/eds-next-react"] }
// postcss.config.mjs
export { default } from "@mercadoeletronico/eds-next-react/postcss.config";
// app/layout.tsx
import "@mercadoeletronico/eds-next-react/globals.css"
/* The host's global CSS, Tailwind v4 */
@import "tailwindcss";
@source "../node_modules/@mercadoeletronico/eds-next-react/src";

10.11 Charts

The shadcn pattern: pnpm dlx shadcn@latest add chart installs ChartContainer, ChartConfig, ChartTooltip and ChartLegend into packages/ui/src/components/, backed by Recharts. Colors come from the --chart-1 to --chart-N tokens (the same var(--color-...) logic as section 10.4). Never install Chart.js, ApexCharts, Nivo, Highcharts or any other charting library, and never build a chart from scratch without going through the shadcn CLI. The Vue equivalent is shadcn-vue chart, backed by Unovis/vue.


Each link below opens the component's public documentation on eletro.design, with real screenshots on the page itself. This file embeds no images, only the reference list.


Source: ME Brandbook (August 2024). The product SVGs used day to day are inline in section 12.1, copy the code directly. Known gap: the PNG files (the Logos-ME/Marketing/ and Logos-ME/Produto/ folders) have no public URL documented here yet, today they only exist locally. If you need a specific PNG, flag the gap instead of assuming a path.

12.1 Logo for product

A simplified version of the me. logo, with no institutional context, for use inside the product itself (favicon, splash screen, empty state, loading, a light watermark). Three solid color variants, transparent background. Pick by the surface you are applying it to:

VariantWhen to useFile
WhiteOn dark backgrounds or on --ui-primaryLogos-ME/Produto/logo-me-produto-branco.png
BlueOn light or neutral backgrounds (--ui-bg, --ui-bg-elevated)Logos-ME/Produto/logo-me-produto-azul.png
BlackMonochrome or greyscale contextsLogos-ME/Produto/logo-me-produto-preto.png

The real SVG (Blue Primary variant). Copy this code literally, never type "me." as text:

<svg width="224" height="96" viewBox="0 0 224 96" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M122.013 43.674V84.8039H101.178V46.8764C101.178 35.2589 95.9725 29.919 87.5575 29.919C78.2125 29.919 71.665 35.9264 71.665 48.7514V84.8039H50.8376V46.8764C50.8376 35.2589 45.895 29.919 37.2175 29.919C28 29.919 21.4601 35.9264 21.4601 48.7514V84.8039H0.632568V12.9614H20.53V21.2414C25.87 15.0989 33.8801 11.8965 43.0976 11.8965C53.1101 11.8965 61.6601 15.7664 66.7301 23.6489C72.4751 16.3064 81.8201 11.8965 92.6351 11.8965C109.728 11.8965 122.013 21.7815 122.013 43.6815" fill="#1052E0"/><path d="M155.777 28.3815C163.675 28.3815 170.417 32.7915 172.892 39.459H138.61C141.257 32.7915 148.022 28.3815 155.777 28.3815ZM183.955 23.949C176.927 15.7665 166.72 11.079 155.95 11.079C145.795 11.079 136.39 15.0015 129.475 22.119C122.747 29.049 119.042 38.199 119.042 47.904C119.042 57.609 122.747 66.819 129.475 73.7565C136.382 80.8815 145.787 84.8115 155.95 84.8115C172.292 84.8115 186.4 74.649 191.05 59.529L191.35 58.554H171.55L171.332 58.944C168.317 64.3815 162.392 67.764 155.86 67.764C146.657 67.764 139.037 61.8765 137.41 53.649H192.235L192.332 53.0115C193.922 42.609 190.87 32.0265 183.947 23.964" fill="#1052E0"/><path d="M209.523 84.804C202.12 84.804 195.678 79.314 195.678 70.959C195.678 62.604 202.12 57.114 209.523 57.114C216.925 57.114 223.368 62.604 223.368 70.959C223.368 79.314 216.925 84.804 209.523 84.804Z" fill="#1052E0"/></svg>

For the White and Black variants, use the same SVG above, swapping fill="#1052E0" for fill="#FFFFFF" (white) or fill="#314158" (black) in all three <path> elements.

  • Never apply a gradient, shadow or effect over the product logo.
  • Keep the minimum clear space (see 12.3) even in a product context.
  • This is the only logo allowed inside the product UI. Never use the marketing versions (with a colored background or frame) embedded in product screens.

12.2 Use in the platform header

The real component in Figma: EDSNext · Foundations — Logo. The logo color in the header follows the bar background, never the marketing versions:

Header contextLogoFile (SVG)
Blue background (--ui-primary, #1052E0), the dashboard top bar and main navigationWhiteLogos-ME/Produto/logo-me-produto-branco.svg
White or light background, public screens, login, compact and mobile headerBlue PrimaryLogos-ME/Produto/logo-me-produto-azul-primary.svg
White or light background, monochrome or neutral contextBlackLogos-ME/Produto/logo-me-produto-preto.svg

Official SVGs available in Logos-ME/Produto/: logo-me-produto-branco.svg, logo-me-produto-azul-primary.svg (#1052E0, recommended for product), logo-me-produto-azul-default.svg (#0C3DA8, an alternative variant, not to be confused with the institutional blue #0A0A93) and logo-me-produto-preto.svg (#314158).

12.3 Institutional logo (marketing)

Use it in institutional pieces, presentations, marketing material and the brand book.

Versions, in order of preference:

  1. Main (negative): white on a blue background. The preferred version. File: Logos-ME/Marketing/logo-me-marketing-negativa.png
  2. Alternative (positive): blue on a white background. Only when the negative is not possible. File: Logos-ME/Marketing/logo-me-marketing-positiva.png
  3. Black: exclusively for a technical application limit (monochrome printing, for example). File: Logos-ME/Marketing/logo-me-marketing-preto.png
  4. White on black: the same technical exception, for dark backgrounds that are not blue. File: Logos-ME/Marketing/logo-me-marketing-branco-sobre-preto.png

Specifications:

  • Clear space: a module of 2x the height by the width of the logo's dot.
  • Minimum reduction: 100mm / 30px (simple signature) up to 350mm / 100px (signature with a product, me.marketplace for example). Never reduce below that.
  • Application over colored backgrounds: only the main institutional colors (see the palette below).
  • Application over an image or photographic background: forbidden.
  • Misuse: do not rotate it, do not change the proportion between letters, do not change the typography, do not change the color, do not apply patterns or effects, do not use it in the middle of a sentence, do not intrude on the clear space.

ME product signature: the ME logo plus a descriptive term (me.source-to-pay, me.marketplace, me.ai-genius, for example).

Institutional brand colors (different from the EDS product tokens, see sections 5 and 10):

ColorPantoneHexRGBCMYKUse
Main blueBlue 072 C#0A0A9310, 10, 147100, 99, 5, 7The lead color, 80% of the layout
Support pink807 C#FF38C6255, 56, 19810, 83, 0, 0Support and contrast, 20% of the layout
Support blue2995 C#08BFE08, 191, 22469, 0, 9, 0Support and contrast, background for the positive logo
Support green374 C#BDE44E189, 228, 7830, 0, 84, 0Support and contrast, background for the positive logo

Careful: the institutional blue (#0A0A93) is not the product blue (--ui-primary, #1052E0). Never use one in place of the other.


13. Components with known specific rules

  • FilterBar: the cancel button (X) is icon only, with no label, tooltip "remover", visible from the start of the flow. The remove button (X) only appears after the first criterion is selected. No unit tests.
  • DatePicker / InputDate: when handling an unavailable state, follow the state naming from Figma, and never decide a prop name in the specification (that is the developer's decision at implementation time).
  • EDSlegacy: no screen, component or variant may reference dark mode anywhere (not in CSS, not in a prop, not in documentation copy).
  • Header / MeHeader: generation tools frequently get the icon wrong on this component. The per-slot map is consolidated in section 13.1. Public documentation: https://eletro.design/blocks/header. Corrected on 2026-07-30: Transações LogIn to FileInput, Mensagens MessageSquare to MessagesSquare, Genius Sparkles to WandSparkles.
  • A known Header bug, do not replicate it: Header.vue uses 'var(--text-inverted)' as the computedIconColor fallback when brand.iconColor is not provided. That variable does not exist (the Nuxt UI token is --ui-text-inverted), so the fallback resolves to nothing. When consuming the Header, pass brand.iconColor explicitly. When fixing the component, the correct name is --ui-text-inverted.

13.1 Header icon map per slot

Verified in docs/content/5.blocks/header.md, blocks/Header/Header.vue and blocks/Header/HeaderAvatar.vue. All of them are Lucide. The navigation and profile items are configurable by the consuming app; what follows is the documented default set, which is what you should use when there is no instruction to the contrary.

Component chrome (fixed, not configurable):

SlotIcon
Mobile menulucide:menu
Mobile searchlucide:search
Mobile cartlucide:shopping-cart
Theme mode in the avatar menulucide:moon in dark, lucide:sun in light
Active item in an avatar submenulucide:check

Navigation items (documented default). The labels are the real product labels and stay in Portuguese:

ItemIcon
Dashboardlucide:layout-dashboard
Transaçõeslucide:file-input
Catálogoslucide:shopping-bag
Usuárioslucide:users
Mais (opens the site map)lucide:ellipsis
Mensagenslucide:messages-square
Geniuslucide:wand-sparkles

Profile menu items (documented default):

ItemIcon
Trocar Contalucide:arrow-left-right
Idioma (Português - BR)lucide:globe
Substituiçãolucide:refresh-cw
Ver Perfillucide:user
Configurações Geraislucide:settings
Novidades e Atualizaçõeslucide:gift
Encerrar Sessãolucide:log-out

That is 19 verified slots, not the 23 this file used to estimate. If a slot shows up outside this list, it comes from the consuming app's configuration and not from the DS. Note: the profile item icons come in the docs with color: "#6b7280" hardcoded as a hex, which contradicts the rule in section 4. When consuming them, prefer leaving color out and inheriting text-muted.


14. Product tone of voice (inherited from the ME brand)

The tone of voice is the same one defined in the institutional Brandbook, applied to the reality of product microcopy: buttons, toasts, validation, empty states, tooltips.

Expert · Simple · Helpful

PillarWhat it means in product
ExpertUse the correct term from the procurement domain (RFQ, cotação, fornecedor) without turning it into impenetrable jargon. If you need to simplify, simplify the explanation, not the term.
SimpleA short sentence, a clear action verb, no performative "please" and no elaborate grammar.
HelpfulEvery error or blocking message points at the next step. It never leaves the user stuck with no way out.

UX writing guardrails (inspired by the Nielsen Norman Group)

Product copy is written in Brazilian Portuguese, so the examples below stay in Portuguese on purpose.

  1. Clarity before personality. Product microcopy is not the place for brand creativity. The priority is that the user understands what happened and what to do in under 2 seconds.
  2. Errors never blame the user. Replace "Você preencheu errado" with "O CNPJ precisa ter 14 dígitos". Focus on the corrective action, not on the failure.
  3. One problem, one sentence. Avoid compound messages with several causes. If there is more than one validation, list them.
  4. Buttons describe the action, not the system. "Enviar cotação", not a generic "Confirmar". The user should not have to read the context to know what the button does.
  5. An empty state is an opportunity, not a desert. Always explain why it is empty and what the next possible action is (for example "Nenhuma cotação ainda. Crie a primeira cotação para começar.").
  6. Terminological consistency over stylistic variation. If the system calls it "fornecedor" on one screen, do not call it "supplier" or "parceiro" on another.
  7. Confirming a destructive action is always specific. Never a bare "Tem certeza?". Always "Excluir a cotação #4521? Essa ação não pode ser desfeita."
  8. Tooltips exist to reduce cognitive load, not to explain the obvious. If the label is already clear, do not add a redundant tooltip.
  9. A loading state has copy, not just a spinner, when the wait can exceed roughly 2s (for example "Buscando fornecedores compatíveis...").
  10. Clear Portuguese over a literal translation of English terms, when a natural PT-BR term is already established in the corporate procurement domain.

15. Final checklist before calling it done

The full rules are in sections 4 (Vue) and 10.2 to 10.7 (React). This is only the last pass, so do not repeat the rules here, just confirm each one was followed.

  • No raw values (HEX / RGB / HSL, fixed px), only a token or a utility class (section 4 / 10.4).
  • No var(--eds-*) in the generated code, that namespace does not exist (section 4).
  • Library components configured through props, not through a hand-written token (section 9).
  • Dark mode covered, except on EDSlegacy: the :root and .dark pair, never a media query (sections 4, 5.3 and 10.7).
  • :focus-visible, AA contrast, keyboard navigation (section 4).
  • Copy in PT-BR, in the Expert / Simple / Helpful tone (section 14).
  • Component verified as not already existing in the library before creating a new one (section 4).
  • React only: data-slot / data-variant / data-size, imports through the package subpath, cva for variants, a named export (sections 10.2 and 10.3).

Recurring mistakes to avoid (not covered elsewhere)

  • Never mention, suggest or link Storybook, EDS does not use it.
  • Never paint large background areas with the primary color outside a marketing or hero context, that is not a product rule.
  • Never decide a prop name in specification documentation, that is the developer's call; the spec only references the state in Figma.
  • Never write an error message that blames the user or that ends without saying what to do next.

16. Sources of truth and references

  • This page, the canonical design contract: eletro.design/ai/designmd, source at docs/content/3.ai/designmd.md
  • Public component documentation: eletro.design
  • Base library documentation (Vue): ui.nuxt.com
  • Icons: https://lucide.dev/icons/, the official Lucide gallery, is the source of truth for whether an icon exists and what it is called. Names are kebab-case on the site, with an individual page at lucide.dev/icons/{name}. Do not use a third-party aggregator (icones.js.org and the like) as a reference: they lag behind releases and list collections EDS does not use, which has already led to a non-existent icon name in generated code.
  • Full token list (Vue): references/tokens-full.md (through the edsnext-tokens skill). Careful: out of date. It is the other source that still documents the non-existent --eds-* namespace (section 4). Prefer sections 5 to 8 of this file until it is fixed.
  • Full semantic map per component (Vue): references/semantic-map.md
  • Nuxt UI v3 / Tailwind v4 configuration (Vue): references/nuxt-ui-config.md
  • React repository: me-eds-next-react (internal GitLab) · published package @mercadoeletronico/eds-next-react
  • Agent guide in the React repo: AGENTS.md at the root of the monorepo (component checklist, commit and branch conventions, PR checklist)

17. Do's and Don'ts

A short index of the rules that cost the most rework when missed. Each line names the section that holds the reasoning, so this list stays an index and never becomes a second source of truth.

Do

  • Use the Tailwind utility class for spacing, radius, typography and shadow. (4, 6, 7, 8)
  • Use var(--ui-*) for semantic color when a value has to enter your own CSS. (4, 5.1)
  • Configure a library component through its props. A component that exists in the package is never styled by a hand-written token. (9)
  • Use an opacity modifier for hover and active: /75 solid, /15 soft, /10 ghost. (5.1)
  • Hardcode color inside an SVG, a drawn icon or an illustration. That is correct there, not a violation. (4)
  • Flag a gap when a value is missing, and say which section is missing it. (0, 10.4)

Don't

  • Don't write --eds-*. No file in the package defines that namespace, so it resolves to nothing and fails silently. (4)
  • Don't use an adjacent scale stop for hover (-600 instead of -500). It diverges from the components and moves the wrong way in dark mode. (5.1)
  • Don't paint a large block with the primary color. The Header bar is the only exception in the whole product. (3)
  • Don't suggest, generate or validate dark mode for EDSlegacy. It does not exist there. (2)
  • Don't document a component in Storybook, and don't link to it. The empty package in the React monorepo is idle scaffolding, not a convention. (2)
  • Don't install another charting library, and don't build a chart from scratch. (4, 10.11)
  • Don't render the ME logo as typeset text. Copy the real SVG. (4, 12.1)
  • Don't copy a token name from one stack to the other without checking the equivalence table. (2, 10.4)

18. Iteration Guide

How to run the work, as opposed to how the result should look. Section 15 checks the output after the fact; this section is what keeps you out of trouble before that.

  1. One component at a time. Check whether it already exists in the package, and in the public docs, before writing anything.
  2. Start from the target. EDSnext, EDSlegacy or EDSreact: token naming, the dark mode mechanism and the way you style all differ. Section 2 has the table.
  3. When a value is missing, flag the gap. Never fill it with an invented value. A documented gap is information; an invented value is a defect that propagates into code.
  4. Prefer a prop over a class, and a class over your own CSS. Reach for <style> only when the first two genuinely cannot express it.
  5. When in doubt about emphasis, use weight and text color before size. The product is dense; scale is the most expensive tool in the box.
  6. Verify against the code, not against the MCP or the docs site. Both can drift from the package, and both have drifted. Section 16 lists which source wins.
  7. A variant is not a new component. -active, -disabled and -focused are states of something that already exists.