Начало работы: Vue
Vue 3 + Vite без React: valyro-ui/styles.css, атрибуты темы на <html> и статичная разметка компонентов в шаблонах. Интерактивные компоненты — React-островом
1. Создайте приложение
Проверено с create-vue 3.24, Vue 3.5 и Vite 8.
npm create vue@latest -- --ts my-app
cd my-app
npm install
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. Подключите стили
Шаблонный CSS красит body и задаёт ширину #app — он перекрывает фон темы (тема рисует фон на <html>). Удалите src/assets и src/components и строку import './assets/main.css'.
import 'valyro-ui/styles.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
4. Тема без вспышки
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">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script>
// Runs before first paint: apply the saved choice so the page never flashes the default theme.
(() => {
const root = document.documentElement;
const theme = localStorage.getItem('ui-theme');
const mode = localStorage.getItem('ui-mode');
if (theme === 'glass' || theme === 'vercel') root.dataset.theme = theme;
if (mode === 'light' || mode === 'dark') root.dataset.mode = mode;
})();
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
5. Переключатель темы
Composable читает текущие атрибуты <html> (их уже выставил скрипт) и при изменении пишет их обратно вместе с localStorage.
import { ref, watch } from 'vue'
export type ThemeName = 'vercel' | 'glass'
export type ThemeMode = 'light' | 'dark'
const root = document.documentElement
// The inline script in index.html has already applied the saved choice, so <html> is the source of truth.
const theme = ref<ThemeName>(root.dataset.theme === 'vercel' ? 'vercel' : 'glass')
const mode = ref<ThemeMode>(root.dataset.mode === 'dark' ? 'dark' : 'light')
watch(theme, (value) => {
root.dataset.theme = value
localStorage.setItem('ui-theme', value)
})
watch(mode, (value) => {
root.dataset.mode = value
localStorage.setItem('ui-mode', value)
})
export function useTheme() {
return {
theme,
mode,
toggleTheme: () => {
theme.value = theme.value === 'glass' ? 'vercel' : 'glass'
},
toggleMode: () => {
mode.value = mode.value === 'light' ? 'dark' : 'light'
},
}
}
6. Разметка компонентов
Разметка — ровно то, что рендерят React-компоненты (Button, Badge, TextInput, Alert, Card). Без React работают только визуальные компоненты (список — в гайде HTML + CSS); разметку любого из них показывает пример «Использование» на странице компонента, когда выбран этот фреймворк. Во Vue она вставляется в шаблон как есть.
<script setup lang="ts">
import { useTheme } from './useTheme'
const { theme, mode, toggleTheme, toggleMode } = useTheme()
</script>
<template>
<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>
</template>
<style scoped>
.demo {
display: grid;
justify-items: start;
gap: 16px;
max-width: 560px;
padding: 32px;
}
.toolbar {
display: flex;
gap: 8px;
}
</style>
7. Интерактивные компоненты — React-островом
Select, Switch, Dialog и другие компоненты с состоянием работают только в React. Их можно смонтировать в Vue-компонент через createRoot — без JSX и без плагина React для Vite. Бандл вырастет на React 19 (≈ +82 kB gzip). Передавайте в ThemeProvider текущие тему и режим: он пишет их на <html>, и без них остров переключил бы страницу на vercel/light.
npm install react react-dom
npm install -D @types/react @types/react-dom
8. Обёртка острова
<script setup lang="ts" generic="P extends object">
import { createElement, type ComponentType } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { ThemeProvider } from 'valyro-ui'
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
import { useTheme } from './useTheme'
const props = defineProps<{ component: ComponentType<P>; props: P }>()
const { theme, mode } = useTheme()
const container = useTemplateRef<HTMLDivElement>('container')
let root: Root | null = null
function render() {
root?.render(
createElement(ThemeProvider, {
theme: theme.value,
mode: mode.value,
children: createElement(props.component, props.props),
}),
)
}
onMounted(() => {
root = createRoot(container.value!)
render()
})
// Re-render when the theme/mode changes or the parent passes new props.
watch([theme, mode, () => props.component, () => props.props], render)
onBeforeUnmount(() => {
root?.unmount()
root = null
})
</script>
<template>
<div ref="container" data-react-island></div>
</template>
9. Остров в шаблоне
Состояние живёт во Vue: значения уходят в пропсы, изменения возвращаются через колбэки. Типы пропсов Select и Switch проверяются vue-tsc.
<script setup lang="ts">
import { ref } from 'vue'
import { Select, Switch } from 'valyro-ui'
import ReactIsland from './ReactIsland.vue'
const region = ref('eu')
const notify = ref(true)
const regions = [
{ value: 'eu', label: 'Europe' },
{ value: 'us', label: 'United States' },
]
</script>
<template>
<ReactIsland
:component="Select"
:props="{ 'aria-label': 'Region', options: regions, value: region, onValueChange: (v: string) => (region = v) }"
/>
<ReactIsland
:component="Switch"
:props="{ label: 'Email notifications', checked: notify, onCheckedChange: (v: boolean) => (notify = v) }"
/>
<p>Vue state: region = <b>{{ region }}</b>, notifications = <b>{{ notify }}</b></p>
</template>
10. Тихая сборка (по желанию)
С островом сборка печатает предупреждение Rolldown на каждый модуль пакета с "use client" (больше сотни). Директива нужна React Server Components и в клиентском бандле безвредна; этот фильтр скрывает только эти предупреждения.
export default defineConfig({
// …plugins and resolve as in the template…
build: {
rolldownOptions: {
// valyro-ui marks its modules 'use client' for React Server Components; a client bundle can ignore it.
onwarn(warning, warn) {
if (warning.code !== 'MODULE_LEVEL_DIRECTIVE') warn(warning)
},
},
},
})
11. Соберите
npm run build запускает vue-tsc и vite build. Предупреждение Rolldown о директиве "use client" при импорте JS пакета безвредно (фильтр — выше).
npm run build
npx vite preview