Aspect Ratio
templates/components/AspectRatio
AspectRatio é um primitivo de layout puro: reserva uma caixa de proporção fixa (largura → altura) para mídia responsiva e previne o layout shift (CLS). Não possui estética própria — só mantém a proporção. Por isso todo enricher gira em torno de carregar, sobrepor e ajustar a mídia DENTRO dessa caixa reservada.
Base congelada
Capacidades 9
Conteúdo
Ajuste da mídia (object-fit)
Define como a imagem/vídeo preenche a…
Placeholder de carregamento
Ocupa a caixa reservada enquanto a…
Lazy-load (carregar ao entrar na tela)
Adia o download da mídia até…
Estado de erro / fallback
Se a mídia falhar, mostra ícone…
Legenda sobreposta
Faixa de texto sobre a mídia,…
Etiqueta de canto (badge)
Selo curto sobre a mídia (ex.:…
Razão responsiva por breakpoint
Altera a proporção conforme a largura…
Comportamento
Ampliar ao clicar (lightbox)
Clique abre a mídia em tela…
Fachada de vídeo (botão play)
Mostra capa + botão play e…
Claude Code
Cole no Claude Code — ele acerta de primeira.
AspectRatio (UI) — Design System (Symfony UX Toolkit / shadcn)
BASE CONGELADA (não mude por instância): A caixa proporcional em si: div `relative` com `aspect-ratio: {ratio}` (altura derivada da largura), `data-slot="aspect-ratio"`, passthrough de `{{ attributes }}` e merge de classes via `tailwind_merge`. Congelados: a AUSÊNCIA de estética própria (sem borda, raio, fundo, padding ou tipografia impostos pela base), o comportamento fluido/responsivo (proporção guiada pela largura) e a semântica/a11y. Todo enricher vive DENTRO da caixa (overlays, placeholders, estados) ou apenas envolve o carregamento da mídia — nunca redefine raio, cor, espaçamento ou fonte da base.
USO: <twig:AspectRatio />
CAPACIDADES OPT-IN (ligue sem alterar o visual):
- Ajuste da mídia (object-fit): Define como a imagem/vídeo preenche a caixa proporcional sem quebrar o layout. [select → cover (corta p/ preencher) / contain (encaixa inteiro) / fill (estica) / scale-down]- Placeholder de carregamento: Ocupa a caixa reservada enquanto a mídia baixa, reforçando a prevenção de CLS. [select → Nenhum / Esqueleto (shimmer) / Blur-up (LQIP) / Cor sólida (token muted)]- Lazy-load (carregar ao entrar na tela): Adia o download da mídia até a caixa entrar na viewport (IntersectionObserver). [toggle]- Estado de erro / fallback: Se a mídia falhar, mostra ícone e mensagem dentro da caixa em vez de imagem quebrada. [toggle]- Legenda sobreposta: Faixa de texto sobre a mídia, dentro da proporção; ancorável no topo ou rodapé. [text]- Etiqueta de canto (badge): Selo curto sobre a mídia (ex.: AO VIVO, HD, 3:24, GIF) fixado num dos cantos. [text]- Ampliar ao clicar (lightbox): Clique abre a mídia em tela cheia; a caixa proporcional vira o gatilho de zoom. [toggle]- Fachada de vídeo (botão play): Mostra capa + botão play e só injeta o iframe do YouTube/Vimeo ao clicar (perf). [toggle]- Razão responsiva por breakpoint: Altera a proporção conforme a largura da tela (ex.: vertical no mobile, wide no desktop). [select → Mantém (todas as telas) / Vertical no mobile (9/16 → 16/9) / Quadrado no mobile (1/1 → 16/9) / Wide no desktop (4/3 → 21/9)]FAÇA:
- Use <twig:Nome> — a base é congelada, você é dono do template em templates/components/.
- Ligue apenas capacidades opt-in listadas (e_funcao=true); a aparência não muda ao ligá-las.
- Passe atributos extras (id, aria-*, name, data-*) via {{ attributes }}.
NÃO FAÇA:
- Não mude cor/fonte/cantos/espaçamento por instância — é decisão de BASE, na fonte única assets/styles/app.css.
- Não crie variante/fork para a mesma coisa — existe UMA base por componente.
- Não reimplemente o componente nem adicione toolchain Node.
TESTE ANTES DE MUDAR: "é função ou é base?" — função = capacidade opt-in; base = mude o token na fonte única (assets/styles/app.css), para todos.
A11Y (herdada da base): Congelados: a AUSÊNCIA de estética própria (sem borda, raio, fundo, padding ou tipografia impostos pela base), o comportamento fluido/responsivo (proporção guiada pela largura) e a semântica/a11y
LLM / MCP
Via MCP: tool get_component com {"id": "aspect-ratio"} · list_capabilities("aspect-ratio").
Spec crua: config/ds-specs/aspect-ratio.json · Conectar o MCP.
Spec machine-readable (JSON)
{
"$schema_version": "1.0",
"id": "aspect-ratio",
"component": "AspectRatio",
"eixo": "ui",
"particularidade": "AspectRatio é um primitivo de layout puro: reserva uma caixa de proporção fixa (largura → altura) para mídia responsiva e previne o layout shift (CLS). Não possui estética própria — só mantém a proporção. Por isso todo enricher gira em torno de carregar, sobrepor e ajustar a mídia DENTRO dessa caixa reservada.",
"base_congelada": "A caixa proporcional em si: div `relative` com `aspect-ratio: {ratio}` (altura derivada da largura), `data-slot=\"aspect-ratio\"`, passthrough de `{{ attributes }}` e merge de classes via `tailwind_merge`. Congelados: a AUSÊNCIA de estética própria (sem borda, raio, fundo, padding ou tipografia impostos pela base), o comportamento fluido/responsivo (proporção guiada pela largura) e a semântica/a11y. Todo enricher vive DENTRO da caixa (overlays, placeholders, estados) ou apenas envolve o carregamento da mídia — nunca redefine raio, cor, espaçamento ou fonte da base.",
"props": [
{
"name": "ratio",
"type": "string",
"default": null,
"description": "The aspect ratio (e.g., `16 / 9`, `4 / 3`, `1 / 1`) #}"
}
],
"capacidades": [
{
"id": "ajuste-da-m-dia-object-fit",
"nome": "Ajuste da mídia (object-fit)",
"descricao": "Define como a imagem/vídeo preenche a caixa proporcional sem quebrar o layout.",
"controle": "select",
"opcoes": [
"cover (corta p/ preencher)",
"contain (encaixa inteiro)",
"fill (estica)",
"scale-down"
],
"posicionavel": false,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "placeholder-de-carregamento",
"nome": "Placeholder de carregamento",
"descricao": "Ocupa a caixa reservada enquanto a mídia baixa, reforçando a prevenção de CLS.",
"controle": "select",
"opcoes": [
"Nenhum",
"Esqueleto (shimmer)",
"Blur-up (LQIP)",
"Cor sólida (token muted)"
],
"posicionavel": false,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "lazy-load-carregar-ao-entrar-na-tela",
"nome": "Lazy-load (carregar ao entrar na tela)",
"descricao": "Adia o download da mídia até a caixa entrar na viewport (IntersectionObserver).",
"controle": "toggle",
"opcoes": [],
"posicionavel": false,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "estado-de-erro-fallback",
"nome": "Estado de erro / fallback",
"descricao": "Se a mídia falhar, mostra ícone e mensagem dentro da caixa em vez de imagem quebrada.",
"controle": "toggle",
"opcoes": [],
"posicionavel": false,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "legenda-sobreposta",
"nome": "Legenda sobreposta",
"descricao": "Faixa de texto sobre a mídia, dentro da proporção; ancorável no topo ou rodapé.",
"controle": "text",
"opcoes": [],
"posicionavel": true,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (text). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "etiqueta-de-canto-badge",
"nome": "Etiqueta de canto (badge)",
"descricao": "Selo curto sobre a mídia (ex.: AO VIVO, HD, 3:24, GIF) fixado num dos cantos.",
"controle": "text",
"opcoes": [],
"posicionavel": true,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (text). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "ampliar-ao-clicar-lightbox",
"nome": "Ampliar ao clicar (lightbox)",
"descricao": "Clique abre a mídia em tela cheia; a caixa proporcional vira o gatilho de zoom.",
"controle": "toggle",
"opcoes": [],
"posicionavel": false,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "fachada-de-v-deo-bot-o-play",
"nome": "Fachada de vídeo (botão play)",
"descricao": "Mostra capa + botão play e só injeta o iframe do YouTube/Vimeo ao clicar (perf).",
"controle": "toggle",
"opcoes": [],
"posicionavel": true,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (toggle). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
},
{
"id": "raz-o-responsiva-por-breakpoint",
"nome": "Razão responsiva por breakpoint",
"descricao": "Altera a proporção conforme a largura da tela (ex.: vertical no mobile, wide no desktop).",
"controle": "select",
"opcoes": [
"Mantém (todas as telas)",
"Vertical no mobile (9/16 → 16/9)",
"Quadrado no mobile (1/1 → 16/9)",
"Wide no desktop (4/3 → 21/9)"
],
"posicionavel": false,
"e_funcao": true,
"como_plugar": "Capacidade opt-in (select). Ligue no configurador em /vitrine/ui/aspect-ratio; comportamento via controller Stimulus. Não altera a base."
}
],
"snippet_uso": "<twig:AspectRatio />",
"exemplo_demo": "<div class=\"w-full max-w-sm\">\n <twig:AspectRatio ratio=\"16 / 9\" class=\"rounded-lg bg-muted\">\n <img\n src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MiIgdmlld0JveD0iMCAwIDY0IDY0Ij48cGF0aCBkPSJNMCAwaDY0djY0SDB6IiBzdHlsZT0iZmlsbDp1cmwoI2EpIi8+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJhIiBjeD0iMCIgY3k9IjAiIHI9IjEiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoNzcgNTggLTU4IDc3IDEgMTEpIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdHlsZT0ic3RvcC1jb2xvcjojYjRiNGI0O3N0b3Atb3BhY2l0eToxIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdHlsZT0ic3RvcC1jb2xvcjojOGY4ZjhmO3N0b3Atb3BhY2l0eToxIi8+PC9yYWRpYWxHcmFkaWVudD48L2RlZnM+PC9zdmc+\"\n alt=\"Photo\"\n class=\"absolute inset-0 h-full w-full rounded-lg object-cover grayscale dark:brightness-20\"\n />\n </twig:AspectRatio>\n</div>",
"regras": {
"faca": [
"Use <twig:Nome> — a base é congelada, você é dono do template em templates/components/.",
"Ligue apenas capacidades opt-in listadas (e_funcao=true); a aparência não muda ao ligá-las.",
"Passe atributos extras (id, aria-*, name, data-*) via {{ attributes }}."
],
"nao_faca": [
"Não mude cor/fonte/cantos/espaçamento por instância — é decisão de BASE, na fonte única assets/styles/app.css.",
"Não crie variante/fork para a mesma coisa — existe UMA base por componente.",
"Não reimplemente o componente nem adicione toolchain Node."
]
},
"a11y": "Congelados: a AUSÊNCIA de estética própria (sem borda, raio, fundo, padding ou tipografia impostos pela base), o comportamento fluido/responsivo (proporção guiada pela largura) e a semântica/a11y",
"mcp": {
"tool": "get_component",
"args": {
"id": "aspect-ratio"
}
}
}