ValyroUI English version

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

Без React: valyro-ui/styles.css через @import в styles.css, тема — атрибуты на <html> и маленький сервис на сигналах, разметка компонентов — прямо в шаблонах

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

Проверено с Angular 22.2 (по умолчанию zoneless). С --ssr тот же код тоже работает.

npx @angular/cli@latest new my-app --defaults --style=css --ssr=false --skip-git --ai-config=none
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. Подключите стили

Angular находит файл через поле exports пакета, angular.json для этого менять не нужно.

@import 'valyro-ui/styles.css';

4. Отключите critical CSS

По умолчанию production-сборка встраивает в <head> только часть CSS (без фона темы), а остальное грузит как media="print" и включает скриптом — пока CSS не догрузился, страница рисуется без стилей. С inlineCritical: false стили подключаются обычным <link> в <head>. Бюджет initial поднимите: CSS пакета — около 510 kB (83 kB gzip), и предупреждение о бюджете 500 kB по умолчанию срабатывает на каждой сборке.

"production": {
  "budgets": [
    { "type": "initial", "maximumWarning": "1MB", "maximumError": "2MB" },
    { "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
  ],
  "outputHashing": "all",
  "optimization": {
    "styles": { "inlineCritical": false }
  }
}

5. Тема без вспышки

data-theme (vercel, glass или подключённая отдельным файлом тема) и data-mode (light или dark) на <html> включают тему: от них зависят токены --ui-* и фон страницы. Встроенный скрипт в <head> применяет сохранённый выбор до первой отрисовки.

<!doctype html>
<html lang="en" data-theme="glass" data-mode="light">
<head>
  <meta charset="utf-8">
  <title>MyApp</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <script>
    (function () {
      var html = document.documentElement;
      var theme = localStorage.getItem('ui-theme');
      var mode = localStorage.getItem('ui-mode');
      if (theme === 'vercel' || theme === 'glass') html.setAttribute('data-theme', theme);
      if (mode === 'light' || mode === 'dark') html.setAttribute('data-mode', mode);
    })();
  </script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

6. Сервис темы

DOCUMENT вместо глобального document, а запись — только в браузере: сервис безопасен и при SSR.

import { DOCUMENT, Injectable, PLATFORM_ID, inject, signal } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

export type Theme = 'vercel' | 'glass';
export type Mode = 'light' | 'dark';

@Injectable({ providedIn: 'root' })
export class ThemeService {
  private readonly html = inject(DOCUMENT).documentElement;
  private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

  // index.html sets the defaults and its inline script has already applied the saved choice.
  readonly theme = signal(this.html.getAttribute('data-theme') as Theme);
  readonly mode = signal(this.html.getAttribute('data-mode') as Mode);

  toggleTheme(): void {
    this.theme.update((theme) => (theme === 'glass' ? 'vercel' : 'glass'));
    this.write('theme', this.theme());
  }

  toggleMode(): void {
    this.mode.update((mode) => (mode === 'light' ? 'dark' : 'light'));
    this.write('mode', this.mode());
  }

  private write(name: 'theme' | 'mode', value: string): void {
    if (!this.isBrowser) return;
    this.html.setAttribute(`data-${name}`, value);
    localStorage.setItem(`ui-${name}`, value);
  }
}

7. Корневой компонент

import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';

@Component({
  selector: 'app-root',
  styleUrl: './app.css',
  templateUrl: './app.html',
})
export class App {
  protected readonly themes = inject(ThemeService);
}

8. Разметка компонентов

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

<main class="page">
  <div class="toggles">
    <button class="ui-button" data-variant="secondary" data-size="md" type="button" (click)="themes.toggleTheme()"><span class="ui-button__label">Theme: {{ themes.theme() }}</span></button>
    <button class="ui-button" data-variant="secondary" data-size="md" type="button" (click)="themes.toggleMode()"><span class="ui-button__label">Mode: {{ themes.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>

9. Раскладка страницы

.page {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 16px;
  max-width: 720px;
  padding: 32px;
}

.page > div {
  align-self: stretch;
}

.toggles {
  display: flex;
  gap: 12px;
}

10. Соберите

Шаблонный тест should render title после замены шаблона нужно обновить.

npx ng build
npx serve -s dist/my-app/browser

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