Vitral 0.2
Temas

Temas

Três camadas de tokens compiladas em variáveis CSS, e as quatro maneiras de mudar um token.

Um tema é dado. Um preset é uma árvore de tokens em três camadas, e o motor a compila em variáveis CSS com o prefixo --vt-. Nada renderiza de novo quando ele muda: as variáveis são reescritas e o navegador repinta.

As três camadas

preset.ts
// primitive: palettes, radii, the raw material
{ blue: { 500: '#3b82f6' }, borderRadius: { md: '4px' } }

// semantic: what the application means
{ primary: { color: '{blue.500}' },
  formField: { borderColor: '{surface.300}' },
  colorScheme: { dark: { formField: { borderColor: '{surface.700}' } } } }

// component: what one control needs
{ button: { root: { paddingX: '0.875rem', borderRadius: '{borderRadius.md}' } } }

Um valor entre chaves é uma referência, e sobrevive à compilação como tal: '{primary.color}' vira var(--vt-primary-color), e não a cor que guarda no momento. É isso que faz um único setPrimary() recolorir de uma vez todos os botões, tags, anéis de foco e séries de gráfico.

compilado
/* '{primary.color}' becomes a reference, not a copy */
--vt-primary-color: #3b82f6;
--vt-button-padding-x: 0.875rem;
--vt-button-border-radius: var(--vt-border-radius-md);

Nomes

  • O nome de token de um componente é o seu nome em minúsculas, sem hífens: ToggleSwitch → toggleswitch.
  • Um segmento root é descartado: button.root.paddingX → --vt-button-padding-x.
  • Tudo o que muda de um esquema para o outro vai em colorScheme: { light, dark }, em qualquer profundidade.

Estendendo um preset

theme.ts
import { definePreset, palette, Prism } from '@vitral/vue';

export const Brand = definePreset(Prism, {
    semantic: {
        primary: palette('#7c3aed'),
        formField: { borderRadius: '2px' }
    },
    components: {
        button: { root: { borderRadius: '999px' } }
    }
});

definePreset faz um merge profundo sobre uma base, então um preset de marca declara só as suas diferenças. palette('#7c3aed') deriva os onze tons a partir de uma cor.

Em tempo de execução

useTheme.ts
import { useTheme } from '@vitral/vue';

const { setPreset, setColorScheme, toggleDark, setPrimary, setSurface, setBorders, isDark } = useTheme();

setPrimary('{emerald}');          // a palette reference
setPrimary('#7c3aed');            // or a hex value, and the shades are derived
setSurface({ dark: '{slate}' });  // the greys, per scheme
setColorScheme('system');         // light, dark, or whatever the OS says
setBorders('strong');             // the edges that meet WCAG 1.4.11

Dois conjuntos de bordas

Um preset desenha as bordas do jeito que quer parecer, o que na maioria deles é mais discreto do que o WCAG 1.4.11 pede do contorno de um controle. O conjunto mais forte vive no preset, em strongBorders, compila num bloco próprio e é escolhido com borders: 'strong' na instalação do plugin, ou com setBorders depois. Nada é recompilado em nenhum dos casos — é um atributo no <html>, então a troca é instantânea, e um preset seu pode trazer o próprio par.

theme.ts
export const Brand = definePreset(Ink, {
    semantic: { colorScheme: { light: { formField: { borderColor: '{surface.200}' } } } },
    // Only what changes when the stronger edges are asked for.
    strongBorders: {
        colorScheme: {
            light: { formField: { borderColor: 'color-mix(in srgb, {text.color} 40%, {surface.200})' } },
            dark: { formField: { borderColor: 'color-mix(in srgb, {text.color} 26%, {surface.800})' } }
        }
    }
});

app.use(Vitral, { theme: { preset: Brand, borders: 'strong' } });

Uma instância por vez

dt escreve os mesmos tokens no elemento raiz de um componente, então um único controle pode ser diferente sem um preset só para ele. Abaixo disso, variáveis CSS comuns funcionam como sempre funcionaram.

Example.vue
<!-- one instance, one token -->
<Button label="Square" :dt="{ button: { borderRadius: '0' } }" />

<!-- or a whole subtree, with the CSS variables directly -->
<div style="--vt-primary-color: tomato">
    <Button label="Tomato" />
</div>

Escopo e cascata

O CSS dos componentes é injetado uma vez por componente, na ordem do documento, e pode ser envolvido numa cascade layer com a opção cssLayer. Com ela, qualquer regra da aplicação vence só pela especificidade, e o !important fica fora da sua folha de estilos.