Vitral 0.2
Primeiros passos

Renderização no servidor

Coletar o CSS que uma renderização usou, para que a página chegue estilizada em vez de se estilizar depois.

No navegador, um componente coloca a sua folha de estilos no head na primeira vez que renderiza, então a página só carrega os componentes que usa e nada precisa ser importado à mão. No servidor não há head onde colocá-la, então a renderização coleta as folhas de estilos e a aplicação as escreve no documento que envia.

A renderização

Chame collectStyles(app) depois de renderToString, quando todos os componentes já pediram o que precisam. Ele retorna as custom properties do tema seguidas da folha de estilos de cada componente que renderizou, seja como uma única string css, seja como tags, os elementos <style> prontos para o head.

entry-server.ts
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { Vitral, collectStyles, colorSchemeTag } from '@vitral/vue';
import App from './App.vue';

export async function render() {
    const app = createSSRApp(App).use(Vitral, { theme: { storageKey: 'app-scheme' } });
    const html = await renderToString(app);

    // After the render: by now every component has asked for its stylesheet.
    const { tags } = collectStyles(app);
    return { html, head: colorSchemeTag(app) + tags };
}
index.html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <!--vitral-head-->
    </head>
    <body>
        <div id="app"><!--app-html--></div>
        <script type="module" src="/src/entry-client.ts"></script>
    </body>
</html>

Os elementos levam os mesmos marcadores data-vitral-theme e data-vitral-style que o navegador escreve, então a hidratação os encontra e não injeta uma segunda cópia, e levam o csp.nonce que o plugin recebeu. Os ids vêm do useId() do Vue, que dá ao servidor e ao cliente os mesmos valores, então aria-controls e <label for> sobrevivem à hidratação.

Escuro antes da primeira pintura

Um esquema escuro guardado só é conhecido pelo navegador, então uma página renderizada no servidor chegaria clara e escureceria quando o bundle rodasse — um clarão branco. colorSchemeTag(app) escreve um pequeno script para o head que lê a escolha guardada e a preferência do sistema, e marca o <html> antes que qualquer coisa seja pintada.

Um servidor que já conhece o esquema, por um cookie, uma sessão ou a conta do usuário, não precisa de script e pode renderizar a marca ele mesmo:

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

// The server already knows the scheme, so nothing has to run before the paint.
const dark = request.cookies.get('app-scheme')?.value === 'dark';
const attrs = colorSchemeAttrs(dark); // { class: 'vt-dark' }, or {}

Espalhe esses atributos no <html>. Os dois seguem o darkModeSelector, então um seletor personalizado não pede uma segunda edição.

O que não se desenha no servidor

Um gráfico é desenhado medindo o elemento em que está, e nenhum servidor consegue fazer isso. Ele ainda assim renderiza esse elemento, um host vazio, e o preenche na montagem, então a marcação que o navegador hidrata é a mesma que recebeu. Segurar um componente até depois da montagem (<ClientOnly>) é pior: o seu mounted roda sem ter onde desenhar.

Overlays (diálogos, menus, tooltips) abrem com a interação, então também nunca renderizam no servidor. São teleportados quando a página já está viva.

Nuxt

O módulo faz tudo isso, e ainda os auto-imports.

nuxt.config.ts
export default defineNuxtConfig({
    modules: ['@vitral/nuxt'],
    vitral: {
        preset: 'Prism',          // or '~/theme/brand', or false
        colorScheme: 'system',
        cookie: 'vitral-scheme',  // where the reader's choice is kept
        prefix: 'Vt'              // '' registers <Button> instead of <VtButton>
    }
});
app.vue
<template>
    <VtButton label="Save" icon="check" @click="toast.add({ severity: 'success', summary: 'Saved' })" />
    <VtForm.Root :initial-values="{ email: '' }">
        <VtForm.Field name="email" label="Email" required>
            <VtInputText type="email" />
        </VtForm.Field>
    </VtForm.Root>
</template>

<script setup lang="ts">
const toast = useToast();   // auto-imported, like every composable
</script>

O esquema fica num cookie e não no localStorage, porque só um cookie chega ao servidor. A página então é renderizada no esquema que o leitor escolheu, com <html class="vt-dark"> e tudo. Com 'system', que nenhum servidor consegue resolver, o módulo escreve o script acima no lugar.

As diretivas mantêm os nomes simples (v-tooltip, seja qual for o prefixo) porque são registradas na aplicação em vez de importadas. Um template é compilado antes de qualquer auto-import rodar, então só um registro chega até ele.

Opções

OpçãoTipoDescrição
preset'Prism' | 'Ink' | 'Avalonia' | 'Simple' | 'Astra' | string | falseUm preset pronto, pelo nome, um módulo que exporta um como default, ou false para nenhum tema.
colorScheme'light' | 'dark' | 'system'O esquema inicial, quando o cookie não diz nada.
cookiestring | falseOnde a escolha do leitor fica guardada, para que o servidor possa lê-la. false a esquece entre uma visita e outra.
darkModeSelectorstring | falseOnde o esquema escuro se aplica: uma classe, um atributo, 'system' ou false.
locale'en' | 'ptBR' | string | falseUm locale pronto, pelo nome, ou um módulo que exporta um como default.
prefixstringO prefixo de cada componente registrado: 'Vt' por padrão, '' para <Button>.
components / composablesbooleanRegistra-os globalmente. Desligado, é preciso importar de @vitral/vue à mão.
cssLayer, inputVariant, unstyledstring | false, 'outlined' | 'filled', booleanAs mesmas opções que o plugin aceita; veja a instalação.