Компоненты

sitelo-ui — библиотека компонентов для sitelo. Каждый компонент это функция, возвращающая строку HTML, поэтому он встраивается прямо в дерево javascript-to-html без прослойки: без компилятора, без рантайма, без гидратации. Что вы собрали, то и попадает в dist/.

Она поставляется вместе с sitelo, точка входа — sitelo/ui.

Быстрый старт

npm install sitelo javascript-to-html

Поместите styles() в head и вызывайте компоненты в body. Это вся настройка:

import { body, head, html, meta, title } from 'javascript-to-html'
import { styles, container, stack, heading, text, button } from 'sitelo/ui'

export default () => html({ lang: 'ru' },
  head(
    meta({ charset: 'utf-8' }),
    meta({ name: 'viewport', content: 'width=device-width, initial-scale=1' }),
    title('Мой сайт'),
    styles(),
  ),
  body(
    container({ size: 'md' },
      stack({ gap: 'md' },
        heading({ level: 1 }, 'Привет'),
        text({ variant: 'lead' }, 'Страница, собранная из компонентов.'),
        button({ href: '/docs' }, 'Читать документацию'),
      ),
    ),
  ),
)

Имена компонентов намеренно совпадают с тем, что они рендерят, поэтому некоторые из них — button, input, table, link, code, select, progress — конфликтуют с функциями-элементами javascript-to-html. Импортируйте библиотеку как пространство имён, когда нужны обе:

import * as ui from 'sitelo/ui'

ui.card(
  ui.cardHeader({ title: 'Маршрутизация', subtitle: 'На основе файлов' }),
  ui.cardBody(ui.text('src/about.ht.js становится /about.')),
  ui.cardFooter({ divided: true }, ui.button({ size: 'sm' }, 'Подробнее')),
)

Соглашение о вызове

Каждый компонент принимает необязательный объект пропсов, а за ним — детей, ровно как элемент javascript-to-html. Пропсы, которые компонент понимает, разбираются по имени; всё остальное попадает на отрендеренный элемент как атрибут, поэтому id, data-*, aria-* и атрибуты событий работают без того, чтобы библиотека их перечисляла:

button({ id: 'save', 'data-analytics': 'save-click', onclick: 'save()' }, 'Сохранить')
// <button type="button" id="save" data-analytics="save-click" onclick="save()" class="su-btn …">

Пропс, значение которого компонент не знает — variant: 'nonsense' — откатывается к значению по умолчанию, а не бросает исключение. Косметическая опечатка не должна ронять сборку.

Стили

styles() возвращает элемент <style> со всей таблицей стилей, минифицированной. Это примерно 7 кБ по сети, и она не может потеряться в dist/ — поэтому так по умолчанию. Если хотите подключить её один раз и позволить браузеру кэшировать её между страницами, импортируйте CSS из собираемого входного файла, и Vite его выпустит:

// src/main.js — собирается Vite и кэшируется между страницами
import 'sitelo/ui/styles.css'

Используйте что-то одно, не оба способа сразу.

Темы

Всё держится на пользовательских свойствах CSS в :root: пять палитр, шкала отступов, радиусы, типографика и тени. theme() пишет переопределения для них и принимает имена в camelCase (radiusMd--su-radius-md), объекты палитр или буквальные пользовательские свойства:

import { styles, theme } from 'sitelo/ui'

head(
  styles(),
  // После styles(), чтобы победили эти значения.
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }, {
    dark: { primary: { base: '#8f8ff0' } },
  }),
)

Тёмная тема определяется сама по prefers-color-scheme. Значение light или dark в data-theme или data-su-theme на любом предке перекрывает её — именно это и делает themeToggle():

import { styles, themeScript, themeToggle } from 'sitelo/ui'

head(
  themeScript(), // применяет сохранённый выбор до первой отрисовки
  styles(),
)

// …где угодно в body
themeToggle()

JavaScript, и как его мало

Большинству компонентов он не нужен. Модальное окно и панель — это элементы popover, так что открытие, подложку, клик снаружи и Escape берёт на себя браузер. Аккордеон — это <details name>. Меню — <details>. Подсказки сделаны на CSS.

Скрипт действительно нужен четырём вещам — и каждая забирает его сама:

Во входной файл добавлять нечего — импорт и есть атрибут события:

<!-- rendered by alert({ dismissible: true }) -->
<button class="su-alert-dismiss"
        onclick="import('/su/alert.js').then(m=>m.dismiss(this))">
  &times;
</button>

sitelo отдаёт эти модули из /su/ во время разработки и копирует в сборку только те, на которые ваши страницы действительно ссылаются. Каждый заметно меньше килобайта, ни один не загружается до первого взаимодействия, и до этого момента все компоненты рендерятся корректно: вкладки с панелями показывают ту, что сервер отметил активной, меню открываются и закрываются сами, а кнопка закрытия ничего не делает.

Исключение — toast(): на странице нет ничего, что вызвало бы его за вас:

// src/main.js
import { toast } from 'sitelo/ui/client'

Примеры

Формы сами связывают свои подписи, идентификаторы, подсказки и сообщения об ошибках:

import { card, cardBody, cardFooter, button, stack, textField, selectField } from 'sitelo/ui'

card(
  cardBody(
    stack({ gap: 'md' },
      textField({ label: 'Эл. почта', name: 'email', type: 'email', help: 'Никому не передаётся.' }),
      textField({ label: 'Сайт', name: 'site', startAdornment: 'https://', error: 'Это не URL.' }),
      selectField({ label: 'Тариф', name: 'plan', options: ['Бесплатный', 'Pro'], value: 'Pro' }),
    ),
  ),
  cardFooter({ divided: true }, button({ type: 'submit' }, 'Сохранить')),
)

Модальное окно — это popover, а его триггером служит любая кнопка, указывающая на его id:

import { button, modal } from 'sitelo/ui'

button({ popovertarget: 'confirm' }, 'Удалить…')

modal({
  id: 'confirm',
  title: 'Удалить эту страницу?',
  footer: button({ color: 'danger' }, 'Удалить'),
}, 'Это действие необратимо.')

Вкладки бывают двух видов — ссылки или панели:

// Вкладки-ссылки: по странице на вкладку, без единого скрипта.
tabs({ items: [
  { label: 'Документация', href: '/docs', active: true },
  { label: 'API', href: '/api' },
] })

// Вкладки с панелями: переключаются на месте и сами подгружают нужный код.
tabs({ value: 'use', items: [
  { id: 'install', label: 'Установка', panel: code('npm install sitelo') },
  { id: 'use', label: 'Использование', panel: code("import * as ui from 'sitelo/ui'") },
] })

Таблицы принимают columns и rows, а функция render нужна там, где ячейке мало одного значения:

table({
  striped: true,
  columns: [
    { key: 'page', header: 'Страница' },
    { key: 'size', header: 'Размер', align: 'end' },
    { header: 'Статус', render: (row) => chip({ color: row.ok ? 'success' : 'danger' }, row.ok ? 'ok' : 'ошибка') },
  ],
  rows: pages,
})

Каталог examples/ui в репозитории рендерит все компоненты на одной странице — самый быстрый способ увидеть весь набор.

Справочник компонентов

Все экспорты, по группам. Пропсы типизированы: sitelo/ui поставляется с файлами .d.ts, поэтому редактор подсказывает variant, color и size и в JavaScript, и в TypeScript.

ГруппаКомпоненты
Раскладкаcontainer, stack, grid, divider, card, cardHeader, cardTitle, cardSubtitle, cardMedia, cardBody, cardFooter, aspectRatio
Типографикаtext, heading, link, code, inlineCode, kbd, visuallyHidden, prose
Поля вводаbutton, iconButton, buttonGroup, field, input, textarea, select, textField, textareaField, selectField, checkbox, radio, toggle, choiceGroup, slider, sliderField, toggleButton, toggleGroup
Отображение данныхavatar, avatarGroup, badge, chip, tooltip, table, list, listItem, figure
Обратная связьalert, progress, spinner, skeleton, toasts, empty
Навигацияbreadcrumbs, pagination, tabs, appBar, appBarNav, appBarSpacer, appBarActions, navLink, themeToggle
Оверлеиmodal, drawer, closeButton, menu, menuItem, menuSeparator, accordion, accordionItem, collapsible
Секцииhero, footer, siteFooter, footerColumn, footerBottom, stat, statGroup, steps, timeline, timelineItem, mockup
Стилиstyles, stylesheet, theme, themeScript

Два имени отличаются от ожидаемых: переключатель называется toggle, потому что switch — зарезервированное слово и не может быть импортируемым именем; а стилизованная ссылка экспортируется и как link, и как textLink, чтобы уживаться с link из javascript-to-html. У table, input, select и progress есть тот же запасной выход: dataTable, textInput, selectField, progressBar.