@cloud-ru/ds-theme

中文文档
2.1.0发布于 2026/10/06Apache-2.0TypeScriptESM + CJS近 30 天下载 2.4万

Провайдер оформления — оси (цветовая схема, бренд, плотность, акрил) живут в контексте, полный набор sn-* классов эмитится на DOM-границе.

原始使用文档
文档目录13 节
  1. Когда использовать
  2. Установка
  3. Примеры использования
  4. Бренд для поддерева
  5. Переключение цветовой схемы
  6. Локальная плотность
  7. Props
  8. RootThemeProvider
  9. ChildThemeProvider
  10. Смотри также
  11. Кастомный бренд-цвет
  12. Бренд-цвет из seed
  13. Императивный хук со scope

Theme

@cloud-ru/ds-theme — Провайдер оформления — оси (цветовая схема, бренд, плотность, акрил) живут в контексте, полный набор sn-* классов эмитится на DOM-границе.

@cloud-ru/ds-theme управляет оформлением дизайн-системы. Оси оформления живут в React-контексте, а полный набор CSS-классов sn-* эмитится на DOM-границу. RootThemeProvider ставится один раз в корне и держит оси (colorScheme, brand, platform, density, acrylic), эмитя из них полный набор sn-* на rootRef (обычно <html>). Локальные переопределения в поддереве делает ChildThemeProvider, а компонент, фиксирующий ось у себя, — хук useThemeClassnames.

Кроме предустановленных брендов @cloud-ru/ds-theme умеет собрать бренд-палитру из одного seed-цвета (white-label): из него генерируется полная шкала тонов --sn-brand-color-primary-*, и весь семантический слой каскадит из неё — см. секцию «Кастомный бренд-цвет».

Полное руководство по модели и подключению — в паттерне Оформление — тема, бренд, плотность.

Когда использовать

  • Хост-приложение задаёт оформление в корне: RootThemeProvider value={{ colorScheme, brand, density }} (или store для multi-root / MFE через getGlobalThemeStore().store).
  • Цветовая схема light/dark берётся из useColorScheme (prefers-color-scheme + опциональный персист-адаптер) и передаётся в value.colorScheme.
  • Часть дерева должна иметь другой бренд / плотность / схему — ChildThemeProvider сливает partial-переопределение с ближайшим контекстом.
  • Компонент локально фиксирует ось (мобильная обёртка с density: 'comfort') — useThemeClassnames({ density }) подмешивает текущие colorScheme/brand из контекста.
  • Нужен бренд-цвет вне предустановленного набора (white-label под клиента) — RootThemeProvider brandColor='#RRGGBB' (декларативно) либо хук useApplyCustomTheme({ color }) (императивно).

Полный набор sn-* обязателен на каждой границе: классы токенов не переопределяются по одной оси через CSS-каскад. Никогда не ставьте одиночный sn-comfort руками — для этого есть провайдеры и хук. Старый ThemeProvider / useThemeConfig (произвольные темы по themeMap) — отдельный legacy-механизм, не путать с RootThemeProvider.

Установка

pnpm add @cloud-ru/ds-theme
import {
  RootThemeProvider,
  ChildThemeProvider,
  useThemeClassnames,
  useColorScheme,
  useApplyCustomTheme,
  getGlobalThemeStore,
} from '@cloud-ru/ds-theme'

Примеры использования

Бренд для поддерева

ChildThemeProvider сливает ось brand с ближайшим контекстом и реэмитит полный набор sn-* на своей границе.

import { Block } from '@cloud-ru/ds-block';
import { Button } from '@cloud-ru/ds-button';
import { Counter } from '@cloud-ru/ds-counter';
import { SegmentControl } from '@cloud-ru/ds-segment-control';
import { Tag } from '@cloud-ru/ds-tag';
import { BRAND, Brand, ChildThemeProvider } from '@cloud-ru/ds-theme';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';
import { useState } from 'react';

const BRAND_ITEMS = [BRAND.CloudConsole, BRAND.SnackUI, BRAND.Gitverse, BRAND.HrGreen, BRAND.SiteBlue].map(value => ({
  value,
  label: value,
}));

export function BrandSwitch() {
  const [brand, setBrand] = useState<Brand>(BRAND.CloudConsole);

  return (
    <Flex direction='column' gap='2m' align='flex-start'>
      <SegmentControl items={BRAND_ITEMS} value={brand} onChange={value => setBrand(value as Brand)} />

      {/* ChildThemeProvider сливает ось `brand` с ближайшим контекстом и реэмитит полный набор
          sn-* на своей границе — акцентные цвета компонентов ниже меняются вслед за брендом. */}
      <ChildThemeProvider value={{ brand }}>
        <Block>
          <Flex gap='2m' align='center' wrap>
            <Button appearance='primary' label='Действие' />
            <Tag appearance='primary' label='Бренд' />
            <Counter value={8} appearance='primary' />
          </Flex>
        </Block>
      </ChildThemeProvider>
    </Flex>
  );
}

Переключение цветовой схемы

Поддерево рендерится в выбранной схеме (sn-light / sn-dark). В приложении схема — источник истины useColorScheme.

import { Block } from '@cloud-ru/ds-block';
import { Button } from '@cloud-ru/ds-button';
import { SegmentControl } from '@cloud-ru/ds-segment-control';
import { Tag } from '@cloud-ru/ds-tag';
import { ChildThemeProvider, COLOR_SCHEME, ColorScheme } from '@cloud-ru/ds-theme';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';
import { useState } from 'react';

const SCHEME_ITEMS = [
  { value: COLOR_SCHEME.Light, label: 'Светлая' },
  { value: COLOR_SCHEME.Dark, label: 'Тёмная' },
];

export function ColorSchemeToggle() {
  const [colorScheme, setColorScheme] = useState<ColorScheme>(COLOR_SCHEME.Light);

  return (
    <Flex direction='column' gap='2m' align='flex-start'>
      <SegmentControl
        items={SCHEME_ITEMS}
        value={colorScheme}
        onChange={value => setColorScheme(value as ColorScheme)}
      />

      {/* В приложении colorScheme — источник истины `useColorScheme` (cookie + prefers-color-scheme),
          а корень держит RootThemeProvider. Здесь ChildThemeProvider переключает схему для поддерева:
          материал-подложка Block и компоненты на ней перекрашиваются вслед за схемой. */}
      <ChildThemeProvider value={{ colorScheme }}>
        <Block>
          <Flex gap='2m' align='center' wrap>
            <Button appearance='primary' label='Действие' />
            <Button appearance='neutral' view='outline' label='Отмена' />
            <Tag appearance='blue' label='Метка' />
          </Flex>
        </Block>
      </ChildThemeProvider>
    </Flex>
  );
}

Локальная плотность

Компонент фиксирует density через useThemeClassnames({ density }) — хук подмешивает текущие colorScheme/brand из контекста.

import { Button } from '@cloud-ru/ds-button';
import { SegmentControl } from '@cloud-ru/ds-segment-control';
import { Tag } from '@cloud-ru/ds-tag';
import { DENSITY, Density, useThemeClassnames } from '@cloud-ru/ds-theme';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';
import { useState } from 'react';

const DENSITY_ITEMS = Object.values(DENSITY).map(value => ({ value, label: value }));

function DensitySurface({ density }: { density: Density }) {
  // useThemeClassnames({ density }) подмешивает текущие colorScheme/brand из контекста и навешивает
  // ПОЛНЫЙ набор sn-* (а не одиночный sn-comfort) — внутренние отступы компонентов меняются вслед
  // за плотностью, а тёмная тема при этом не ломается.
  const className = useThemeClassnames({ density });

  return (
    <div className={className}>
      <Flex gap='2m' align='center' wrap>
        <Button appearance='primary' label='Кнопка' />
        <Button appearance='neutral' view='outline' label='Ещё' />
        <Tag appearance='primary' label='Тег' />
      </Flex>
    </div>
  );
}

export function LocalDensity() {
  const [density, setDensity] = useState<Density>(DENSITY.Compact);

  return (
    <Flex direction='column' gap='2m' align='flex-start'>
      <SegmentControl items={DENSITY_ITEMS} value={density} onChange={value => setDensity(value as Density)} />
      <DensitySurface density={density} />
    </Flex>
  );
}

Props

RootThemeProvider

RootThemeProviderProps

PropTypeDefaultDescription
brandColorstring—Кастомный бренд-цвет потребителя (hex #rrggbb) для white-label. Генерирует бренд-палитру из
seed-цвета и инжектит scoped <style> на бренд-классы этого поддерева. Правило на бренд-классе
(а не inline на одном элементе) переживает переэмиты sn-* вложенными компонентами (Table и т.п.),
поэтому кастомный цвет доходит до всей вложенности. Невалидный hex игнорируется. Глобальная
(app-root) альтернатива для порталов — хук useApplyCustomTheme.
childrenstring | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | null | undefined—
classNamestring—Дополнительный класс на wrapper-<div> (паддинги/фон). Только в wrapper-режиме (без rootRef).
noncestring—CSP-nonce для инжектируемого <style> кастомного бренд-цвета.
rootRefRefObject<HTMLElement | null>—Внешний элемент для полного набора sn-* (обычно <html>/<body>). Если не задан — провайдер
оборачивает children в <div> с этим набором.
storeThemeAppearanceStore—Внешний реактивный стор оформления (getGlobalThemeStore().store). Если задан — приоритетнее
value; подписанные провайдеры обновляются при смене темы без перерендера провайдера. Так один
глобальный стор охватывает все микрофронты. Сеттер для shell — getGlobalThemeStore().setAppearance.
valueThemeAppearance—Оформление приложения. Используется в static-режиме (один React-корень: SSR — одно значение на
запрос, либо CSR с собственным state, напр. colorScheme из useColorScheme). Для multi-root
(single-spa) — см. store.
  • Brand = "agents-space" | "cloudConsole" | "giga-id" | "gitverse" | "hrBlue" | "hrGraphite" | "hrGreen" | "hrPurple" | "hrYellow" | "siteBlue" | "siteGraphite" | "siteGreen" | "sitePurple" | "siteYellow" | "snackUI"

  • ColorScheme = "dark" | "light"

  • Density = "comfort" | "compact" | "spacious"

  • Platform = "webDesktop" | "webMobile"

ThemeAppearance

PropTypeDefaultDescription
acrylicboolean | undefined—Акрил (blur-материал) — sn-yes при true, иначе sn-no.
brand"agents-space" | "cloudConsole" | "giga-id" | "gitverse" | "hrBlue" | "hrGraphite" | "hrGreen" | "hrPurple" | "hrYellow" | "siteBlue" | "siteGraphite" | "siteGreen" | "sitePurple" | "siteYellow" | "snackUI"—Бренд — sn-cloudConsole / sn-hrBlue …
colorScheme"dark" | "light"—Цветовая схема — sn-light / sn-dark.
density"comfort" | "compact" | "spacious"—Плотность — sn-comfort / sn-compact / sn-spacious.
platform"webDesktop" | "webMobile"—Платформа — sn-webDesktop / sn-webMobile. Без значения — webDesktop. Раскладку @cloud-ru/ds-adaptive
тема не читает: потребитель передаёт согласованные значения и в AdaptiveProvider, и сюда.

ThemeAppearanceStore

PropTypeDefaultDescription
getServerSnapshot(() => ThemeAppearanceContextValue) | undefined—Значение для SSR/гидрации. Если не задан — используется getSnapshot.
getSnapshot() => ThemeAppearanceContextValue—Текущее значение. Обязан возвращать стабильную (по Object.is) ссылку, пока значение не менялось.
subscribe(onStoreChange: () => void) => () => void—Подписка на изменения; возвращает функцию отписки.

ChildThemeProvider

ChildThemeProviderProps

PropTypeDefaultDescription
childrenstring | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | null | undefined—
classNamestring—Дополнительный класс на wrapper-<div> (паддинги/фон). Только в wrapper-режиме (без rootRef).
rootRefRefObject<HTMLElement | null>—Внешний элемент для полного слитого набора sn-*. Если не задан — провайдер оборачивает
children в <div> с этим набором.
valueThemeAppearance—Оси, переопределяемые в поддереве. Остальные наследуются от ближайшего родителя (слияние).
  • Brand = "agents-space" | "cloudConsole" | "giga-id" | "gitverse" | "hrBlue" | "hrGraphite" | "hrGreen" | "hrPurple" | "hrYellow" | "siteBlue" | "siteGraphite" | "siteGreen" | "sitePurple" | "siteYellow" | "snackUI"

  • ColorScheme = "dark" | "light"

  • Density = "comfort" | "compact" | "spacious"

  • Platform = "webDesktop" | "webMobile"

ThemeAppearance

PropTypeDefaultDescription
acrylicboolean | undefined—Акрил (blur-материал) — sn-yes при true, иначе sn-no.
brand"agents-space" | "cloudConsole" | "giga-id" | "gitverse" | "hrBlue" | "hrGraphite" | "hrGreen" | "hrPurple" | "hrYellow" | "siteBlue" | "siteGraphite" | "siteGreen" | "sitePurple" | "siteYellow" | "snackUI"—Бренд — sn-cloudConsole / sn-hrBlue …
colorScheme"dark" | "light"—Цветовая схема — sn-light / sn-dark.
density"comfort" | "compact" | "spacious"—Плотность — sn-comfort / sn-compact / sn-spacious.
platform"webDesktop" | "webMobile"—Платформа — sn-webDesktop / sn-webMobile. Без значения — webDesktop. Раскладку @cloud-ru/ds-adaptive
тема не читает: потребитель передаёт согласованные значения и в AdaptiveProvider, и сюда.

Смотри также

  • Оформление — тема, бренд, плотность — полная модель подключения темы.
  • Adaptive — раскладка layoutType, согласованная с платформой темы.
  • Locale — рантайм локализации.
  • PortalContext — корневой DOM-узел для порталов.

Кастомный бренд-цвет

Помимо предустановленных брендов (agents-space, cloudConsole, giga-id, gitverse, snackUI, варианты hr* и site*) @cloud-ru/ds-theme собирает бренд-палитру из одного seed-цвета — для white-label под клиента. Из seed генерируется полная шкала тонов --sn-brand-color-primary-* (OKLCH: светлота и насыщенность берутся из опорной шкалы, hue поворачивается к seed) плюс activated-тинты; семантический слой --sn-theme-color-primary-* каскадит из неё. Поэтому один цвет перекрашивает акцент во всех компонентах — и в светлой, и в тёмной схеме.

Палитра применяется CSS-правилом на бренд-классы (.sn-cloudConsole, .sn-hrBlue …), а не inline-переменными на одном элементе. Это принципиально: компоненты, переобъявляющие полный набор sn-* на своих внутренних обёртках (Table, Stepper и т.п. через useThemeClassnames), заново объявляют бренд-палитру из класса — inline-переменные предка в таких поддеревьях перекрываются, а правило на том же бренд-классе — нет. Два способа применить:

  • Декларативно — проп brandColor у RootThemeProvider. Добавляет scoped-правило на бренд-классы поддерева провайдера (доходит до вложенных переобъявлений):

    <RootThemeProvider value={{ colorScheme }} brandColor={brand.primaryColor}>
      {app}
    </RootThemeProvider>
    
  • Императивно — хук useApplyCustomTheme. Без scope добавляет глобальное правило на все бренд-классы страницы (покрывает и порталы — дропдауны, тултипы); со scope ограничивает поддеревом. Удобно, когда бренд-цвет приходит асинхронно из бэкенда в bootstrap-компоненте:

    useApplyCustomTheme({ color: brand.primaryColor, enabled: Boolean(brand), nonce })
    

    Порталы монтируются вне поддерева провайдера, поэтому для их перекраски используйте глобальный useApplyCustomTheme в корне приложения (без scope), а не scoped-brandColor.

Схему (light/dark) кастомный бренд-цвет не задаёт — она остаётся из colorScheme, а палитра тонов от схемы не зависит. Для SSR без мигания палитру можно собрать строкой заранее: generateBrandPalette(color) / buildBrandPaletteVars(color) из @cloud-ru/ds-theme/ssr (чистые, без React и DOM).

Бренд-цвет из seed

brandColor генерирует палитру --sn-brand-color-primary-* из одного цвета — акцент компонентов перекрашивается вслед за выбором.

import { Block } from '@cloud-ru/ds-block';
import { Button } from '@cloud-ru/ds-button';
import { Counter } from '@cloud-ru/ds-counter';
import { SegmentControl } from '@cloud-ru/ds-segment-control';
import { Tag } from '@cloud-ru/ds-tag';
import { RootThemeProvider } from '@cloud-ru/ds-theme';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';
import { useState } from 'react';

const COLOR_ITEMS = [
  { value: '#ff7a00', label: 'Оранжевый' },
  { value: '#8a2be2', label: 'Фиолетовый' },
  { value: '#0077ff', label: 'Синий' },
  { value: '#e5006e', label: 'Розовый' },
];

export function CustomBrandColor() {
  const [color, setColor] = useState('#ff7a00');

  return (
    <Flex direction='column' gap='2m' align='flex-start'>
      <SegmentControl items={COLOR_ITEMS} value={color} onChange={value => setColor(String(value))} />

      {/* brandColor генерирует палитру `--sn-brand-color-primary-*` из одного seed-цвета — акцент
          компонентов ниже перекрашивается вслед за выбором. */}
      <RootThemeProvider value={{ colorScheme: 'light', brand: 'cloudConsole' }} brandColor={color}>
        <Block>
          <Flex gap='2m' align='center' wrap>
            <Button appearance='primary' label='Действие' />
            <Tag appearance='primary' label='Бренд' />
            <Counter value={8} appearance='primary' />
          </Flex>
        </Block>
      </RootThemeProvider>
    </Flex>
  );
}

Императивный хук со scope

useApplyCustomTheme({ color, scope }) инжектит правило, скоупленное на #brand-hook-scope — результат применяется ко всем компонентам внутри поддерева. Без scope правило глобальное (покрывает и порталы).

import { Block } from '@cloud-ru/ds-block';
import { Button } from '@cloud-ru/ds-button';
import { Counter } from '@cloud-ru/ds-counter';
import { SegmentControl } from '@cloud-ru/ds-segment-control';
import { Tag } from '@cloud-ru/ds-tag';
import { RootThemeProvider, useApplyCustomTheme } from '@cloud-ru/ds-theme';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';
import { useState } from 'react';

const COLOR_ITEMS = [
  { value: '#8a2be2', label: 'Фиолетовый' },
  { value: '#ff7a00', label: 'Оранжевый' },
  { value: '#0077ff', label: 'Синий' },
];

// `scope` — CSS-селектор корня поддерева. Здесь правило скоуплено на `#brand-hook-scope`, поэтому
// бренд-акцент перекрашивается только внутри этого блока (и во всех компонентах ниже по дереву — Button,
// Tag, Counter). Без `scope` правило было бы глобальным и перекрасило бы всю страницу, включая порталы.
const SCOPE_ID = 'brand-hook-scope';

export function CustomBrandColorHook() {
  const [color, setColor] = useState('#8a2be2');

  useApplyCustomTheme({ color, scope: `#${SCOPE_ID}` });

  return (
    <Flex direction='column' gap='2m' align='flex-start'>
      <SegmentControl items={COLOR_ITEMS} value={color} onChange={value => setColor(String(value))} />

      <div id={SCOPE_ID}>
        <RootThemeProvider value={{ colorScheme: 'light', brand: 'cloudConsole' }}>
          <Block>
            <Flex gap='2m' align='center' wrap>
              <Button appearance='primary' label='Внутри scope' />
              <Tag appearance='primary' label='Бренд' />
              <Counter value={8} appearance='primary' />
            </Flex>
          </Block>
        </RootThemeProvider>
      </div>
    </Flex>
  );
}

版本与发布记录

@cloud-ru/ds-theme 共发布 22 个版本,首次发布于 2026/08/18;过去 12 个月发布了 22 个版本。

版本发布日期距今
2.1.0latest2026/10/061 个月前
2.0.1-preview-5c9b8135.0preview2026/10/021 个月前
2.0.1-preview-3ffb7c01.02026/10/021 个月前
2.0.1-preview-f367da87.02026/10/021 个月前
2.0.1-preview-ac2c37a8.02026/10/011 个月前
2.0.02026/09/251 个月前

关于 @cloud-ru/ds-theme 的常见问题

@cloud-ru/ds-theme 是什么?

Провайдер оформления — оси (цветовая схема, бренд, плотность, акрил) живут в контексте, полный набор sn-* классов эмитится на DOM-границе.

如何安装 @cloud-ru/ds-theme?

在项目目录运行 npm install @cloud-ru/ds-theme,或使用 pnpm add / yarn add / bun add。

@cloud-ru/ds-theme 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cloud-ru/ds-theme 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@cloud-ru/ds-theme 可以商用吗?

@cloud-ru/ds-theme 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@cloud-ru/ds-theme 的下载量有多少?

根据 npm 官方下载统计,@cloud-ru/ds-theme 在 2026/10/10 – 2026/11/08 这 30 天内被下载 24,053 次,日均约 802 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cloud-ru/ds-theme 还在维护吗?

最新版本 2.1.0 发布于 2026/10/06(1 个月前);至今共发布 22 个版本,其中过去 12 个月发布了 22 个;首次发布于 2026/08/18。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。