Pular para o conteúdo

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

Photo

Conteúdo

Ajuste da mídia (object-fit)

Define como a imagem/vídeo preenche a…

Define como a imagem/vídeo preenche a caixa proporcional sem quebrar o layout.

Placeholder de carregamento

Ocupa a caixa reservada enquanto a…

Ocupa a caixa reservada enquanto a mídia baixa, reforçando a prevenção de CLS.

Lazy-load (carregar ao entrar na tela)

Adia o download da mídia até…

Adia o download da mídia até a caixa entrar na viewport (IntersectionObserver).

Estado de erro / fallback

Se a mídia falhar, mostra ícone…

Se a mídia falhar, mostra ícone e mensagem dentro da caixa em vez de imagem quebrada.

Legenda sobreposta

Faixa de texto sobre a mídia,…

Faixa de texto sobre a mídia, dentro da proporção; ancorável no topo ou rodapé. posicionável

Etiqueta de canto (badge)

Selo curto sobre a mídia (ex.:…

Selo curto sobre a mídia (ex.: AO VIVO, HD, 3:24, GIF) fixado num dos cantos. posicionável

Razão responsiva por breakpoint

Altera a proporção conforme a largura…

Altera a proporção conforme a largura da tela (ex.: vertical no mobile, wide no desktop).

Comportamento

Ampliar ao clicar (lightbox)

Clique abre a mídia em tela…

Clique abre a mídia em tela cheia; a caixa proporcional vira o gatilho de zoom.

Fachada de vídeo (botão play)

Mostra capa + botão play e…

Mostra capa + botão play e só injeta o iframe do YouTube/Vimeo ao clicar (perf). posicionável
Código gerado

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

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"
        }
    }
}