ValyroUI English version

Начало работы: Nuxt

SSR без вспышки: тема в cookie, useHead пишет её на <html> уже в серверном HTML; стили — через css в nuxt.config.ts

1. Создайте приложение

Проверено с Nuxt 4.6. Скаффолдер сам ставит зависимости.

npm create nuxt@latest -- my-app --template minimal --packageManager npm --no-gitInit --modules=
cd my-app

2. Установите пакет

Из пакета нужен только CSS: valyro-ui/styles.css (плюс файл темы, если тема не из двух встроенных). Шрифты — отдельные файлы: тема vercel (по умолчанию в ThemeProvider) использует Geist, поэтому рядом со стилями импортируйте valyro-ui/fonts/geist.css и valyro-ui/fonts/geist-mono.css, иначе текст будет системным шрифтом; glass использует системные шрифты, ей файлы не нужны. npm сам положит react и react-dom в node_modules как peer-зависимости, но в бандл они не попадут, пока вы их не импортируете. Пока пакет не опубликован, установите tarball: npm install ../valyro-ui-0.1.0.tgz.

npm install valyro-ui

3. Подключите стили

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2025-07-15',
  devtools: { enabled: true },
  css: ['valyro-ui/styles.css'],
})

4. Тема в cookie

Cookie, а не localStorage: сервер читает выбор и сразу отдаёт <html> с нужными атрибутами, гидрация совпадает. decode пропускает только известные значения — подменённая cookie откатится к значению по умолчанию. В Nuxt 4 composables лежат в app/composables и импортируются автоматически.

export type UiTheme = 'vercel' | 'glass'
export type UiMode = 'light' | 'dark'

const maxAge = 60 * 60 * 24 * 365

export function useUiTheme() {
  // Cookies (not localStorage) so the server renders the saved choice into <html> on the first response.
  // `decode` whitelists values, so a tampered cookie falls back to the default instead of an unknown theme.
  const theme = useCookie<UiTheme>('ui-theme', {
    default: () => 'glass',
    decode: (value) => (value === 'vercel' ? 'vercel' : 'glass'),
    maxAge,
    sameSite: 'lax',
  })
  const mode = useCookie<UiMode>('ui-mode', {
    default: () => 'light',
    decode: (value) => (value === 'dark' ? 'dark' : 'light'),
    maxAge,
    sameSite: 'lax',
  })

  return {
    theme,
    mode,
    toggleTheme: () => {
      theme.value = theme.value === 'glass' ? 'vercel' : 'glass'
    },
    toggleMode: () => {
      mode.value = mode.value === 'light' ? 'dark' : 'light'
    },
  }
}

5. Атрибуты на <html> и разметка

useHead вызывается один раз в корневом компоненте, чтобы не дублировать записи head. На клиенте атрибуты обновляются примерно через кадр после клика. Разметка — ровно то, что рендерят React-компоненты (Button, Badge, TextInput, Alert, Card). Без React работают только визуальные компоненты (список — в гайде HTML + CSS); разметку любого из них показывает пример «Использование» на странице компонента, когда выбран этот фреймворк.

<script setup lang="ts">
const { theme, mode, toggleTheme, toggleMode } = useUiTheme()

useHead({
  htmlAttrs: { lang: 'en', 'data-theme': theme, 'data-mode': mode },
})
</script>

<template>
  <div>
    <NuxtRouteAnnouncer />
    <main class="demo">
      <div class="toolbar">
        <button class="ui-button" data-variant="secondary" data-size="md" type="button" @click="toggleTheme">
          <span class="ui-button__label">Theme: {{ theme }}</span>
        </button>
        <button class="ui-button" data-variant="secondary" data-size="md" type="button" @click="toggleMode">
          <span class="ui-button__label">Mode: {{ mode }}</span>
        </button>
      </div>

      <button class="ui-button" data-variant="primary" data-size="md" type="button"><span class="ui-button__label">Deploy</span></button>
      <span class="ui-badge" data-color="green" data-variant="subtle" data-size="md"><span class="ui-badge__label">Ready</span></span>
      <div class="ui-field ui-text-input" data-size="md"><input aria-label="Email" placeholder="[email protected]" type="text" class="ui-field__input ui-text-input__input" /></div>
      <div role="status" class="ui-alert" data-variant="info">
        <span class="ui-alert__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="ui-icon"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg></span>
        <div class="ui-alert__content"><div class="ui-alert__title">Heads up</div><div class="ui-alert__description">Static markup, no React at runtime.</div></div>
      </div>
      <div class="ui-card ui-panel" data-variant="default" data-padding="md">
        <div class="ui-card__header"><h3 class="ui-card__title">Project</h3><p class="ui-card__description">Production deployment</p></div>
        <div class="ui-card__content">Body</div>
        <div class="ui-card__footer"><button class="ui-button" data-variant="secondary" data-size="md" type="button"><span class="ui-button__label">Open</span></button></div>
      </div>
    </main>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  justify-items: start;
  gap: 16px;
  max-width: 560px;
  padding: 32px;
}

.toolbar {
  display: flex;
  gap: 8px;
}
</style>

6. Соберите и запустите

npm run build
node .output/server/index.mjs

7. Проверка типов (по желанию)

В шаблоне minimal нет TypeScript-зависимостей. vue-tsc пока не работает с TypeScript 7, поэтому версия закреплена.

npm add -D typescript@~6.0.0 vue-tsc
npx nuxt typecheck

Другие фреймворки