Темы

Все компоненты читают одни и те же кастомные свойства, поэтому тема — это просто набор переопределений на :root: ни шага сборки, ни файла конфигурации, ни компонента, которому пришлось бы об этом сообщать.

Подключить стили

styles() возвращает <link> на один файл, который браузер кэширует на всех страницах сайта. Настраивать нечего и копировать нечего: плагин sitelo отдаёт его в dev и записывает в сборку — по тому же базовому пути, что и рантайм компонентов.

import { styles } from 'sitelo/ui'

head(
  title('Мой сайт'),
  styles(),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">

В имени файла — хеш содержимого, так что его можно отдавать с immutable и при этом выкатывать изменения. Передайте { hash: false }, если нужен просто /su/ui.css, или base, чтобы указать ссылку на копию, которую вы размещаете сами.

{ inline: true } вместо этого кладёт всю таблицу в <style>: около 11 кБ в gzip на каждой странице, зато ни одного лишнего запроса и ничего, что могло бы пропасть из dist/. Для одиночной страницы это выгоднее; ссылка отбивает свой запрос на второй прочитанной странице.

head(
  title('Мой сайт'),
  styles({ inline: true }),
)

Остальные функции семейства выдают детали по отдельности. stylesheet() возвращает сырой CSS строкой — чтобы разместить таблицу там, куда sitelo не дотянется, или записать её куда-нибудь самому, — а stylesUrl() — только URL, для вашего собственного элемента link.

Пресеты

Пресет меняет весь облик разом. styles({ preset: 'neumorphism' }) подключает вторую таблицу стилей сразу после основной — она отдаётся, хешируется и кешируется на тех же условиях, — и каждый компонент на странице следует за ней без единой правки в разметке.

import { styles } from 'sitelo/ui'

head(
  title('Мой сайт'),
  styles({ preset: 'neumorphism' }),
)
// <link rel="stylesheet" href="/su/ui-c9428b65.css">
// <link rel="stylesheet" href="/su/neumorphism-5d0e7b91.css">

У каждого пресета есть своя страница, где все компоненты переоформлены вживую: Неоморфизм, Необрутализм, Superneon.

inline встраивает обе таблицы, stylesheet({ preset }) возвращает их одной строкой, а имя, которое не является пресетом, выбрасывает ошибку со списком существующих.

Переопределение токенов

theme() записывает переопределения. Ключи — это имена токенов в camelCase, объекты палитр или буквальные кастомные свойства, а идёт всё это после styles(), так что побеждает.

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

head(
  styles(),
  theme({
    primary: { base: '#5b5bd6', hover: '#4a4ac4', active: '#3f3fb0', fg: '#ffffff' },
    radiusMd: '2px',
    fontSans: '"Inter", system-ui, sans-serif',
  }),
)

Темы с областью действия

selector ограничивает переопределения поддеревом, а не всей страницей. Именно это и делают три панели ниже: одни и те же компоненты, три разные палитры, одна страница.

индиго
розовая
скруглённая
fragment(
  theme({ primary: { base: '#5b5bd6', hover: '#4a4ac4', fg: '#ffffff', soft: '#e6e6fa', softFg: '#33338f', border: '#b9b9ee' } }, { selector: '.theme-indigo' }),
  theme({ primary: { base: '#b0357a', hover: '#962e68', fg: '#ffffff', soft: '#fbe4f0', softFg: '#7d1f53', border: '#f0a9ce' } }, { selector: '.theme-pink' }),
  theme({ radiusMd: '999px', radiusLg: '1.5rem' }, { selector: '.theme-round' }),
  grid({ min: '11rem' },
    div({ class: 'theme-indigo' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'индиго'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-pink' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'розовая'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
    div({ class: 'theme-round' },
      card(cardBody(stack({ gap: 'sm' },
        text({ variant: 'caption', tone: 'muted' }, 'скруглённая'),
        button({ block: true }, 'Primary'),
        button({ variant: 'soft', block: true }, 'Soft'),
      ))),
    ),
  ),
)

Тёмная тема

Тёмная определяется сама по prefers-color-scheme. Явный data-theme или data-su-theme со значением light или dark на любом предке это перебивает — именно так демо на этом сайте следуют за переключателем в верхней панели.

Передайте dark для переопределений, которые должны действовать только там. Это разом закрывает и атрибут, и медиазапрос.

theme({
  primary: { base: '#5b5bd6' },
}, {
  dark: { primary: { base: '#8f8ff0' } },
})

Что можно переопределить

Пять палитр по девять слотов, шкала интервалов, типографика, скругления, тени и цвета поверхностей. Каждое из этого — кастомное свойство: откройте таблицу стилей или инспектор браузера, и все они окажутся на :root.

primary
neutral
success
warning
danger
xs
sm
md
lg
xl
stack({ gap: 'md' },
  stack({ direction: 'row', gap: 'sm', wrap: true },
    ...['primary', 'neutral', 'success', 'warning', 'danger'].map((color) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: 3.5rem; height: 2rem; border-radius: 0.4rem; background: var(--su-' + color + ')' }),
        text({ variant: 'caption', tone: 'muted' }, color),
      ),
    ),
  ),
  stack({ direction: 'row', gap: 'sm', wrap: true, align: 'flex-end' },
    ...['xs', 'sm', 'md', 'lg', 'xl'].map((step) =>
      stack({ gap: 'xs', align: 'center' },
        div({ style: 'width: var(--su-space-' + step + '); height: 2rem; border-radius: 0.2rem; background: var(--su-neutral)' }),
        text({ variant: 'caption', tone: 'muted' }, step),
      ),
    ),
  ),
)

Именование

Ключ в camelCase превращается в свойство в kebab-case: radiusMd — это --su-radius-md, а fontSans — --su-font-sans. Вложенный объект разворачивается так же: { primary: { softFg: … } } задаёт --su-primary-soft-fg. А ключ, уже начинающийся с --, берётся ровно так, как написан, — это запасной выход для всего, чего преобразование не покрывает.

В палитре девять слотов: base, hover, active, fg, soft, softHover, softFg, border и ring. Задавайте только те, что меняете.

Контраст

Поставляемые палитры проходят WCAG AA относительно поверхностей, на которых лежат, в обеих темах, и в репозитории есть тест, который валит сборку, если это перестанет быть правдой. На вашу собственную тему он не распространяется: сверьте свой fg со своим base, прежде чем выкатывать её.

Пропсы

styles():

ПропТипПо умолчаниюОписание
preset'neumorphism' | 'neubrutalism' | 'superneon'—Переоформить все компоненты пресетом, подключённым или встроенным после основной таблицы.
inlinebooleanfalseВыдать сам CSS, а не ссылку на него.
hashbooleantrueДобавить в имя файла хеш содержимого. Только для ссылки.
basestring'/su/'Указывает URL в другое место; этот файл размещаете вы сами. Только для ссылки.
minifybooleantrueУбрать комментарии и пробелы. Только для inline.
noncestring—CSP-nonce для выдаваемого элемента.

stylesUrl() принимает base и hash; stylesheet() — minify и preset.

theme(tokens, options):

ПропТипПо умолчаниюОписание
selectorstring':root'Ограничивает переопределения поддеревом.
darkobject—Переопределения, действующие только в тёмной теме.
noncestring—CSP-nonce.