Adaptive

@cloud-ru/ds-adaptive несёт через React Context текущую раскладку экрана — layoutType (mobile / tablet / desktopSmall / desktop). AdaptiveProvider ставится один раз в корне приложения, а адаптивные компоненты дизайн-системы (Droplist, Modal, Drawer, Dropdown и др.) сами читают раскладку из контекста и переключают поверхность: на mobile обычно открывается BottomSheet, иначе — десктопный popover / modal.

Полное руководство по модели — в паттерне Адаптивность — руководство.

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

  • Приложение использует адаптивные компоненты дизайн-системы и должно переключать их между mobile и desktop поверхностями. Провайдер ставится один раз в корне.
  • Раскладка считается в корне через useAdaptiveBootstrap() (user-agent + media-query) и раздаётся вложенным компонентам без проброса пропа.
  • Микрофронт получает раскладку из реактивного стора хост-приложения — провайдер подключается через store (адаптер собирается createAdaptiveStore).
  • Серверный рендеринг: раскладка вычисляется из user-agent запроса через подпуть @cloud-ru/ds-adaptive/ssr (чистые функции без React Context).

Точечно зафиксировать платформу для поддерева можно вложенным <AdaptiveProvider layoutType=…> или HOC withLayoutType(Component, …) — он затеняет внешний контекст. Пропа layoutType у самих компонентов нет: форс идёт только через контекст.

Установка

pnpm add @cloud-ru/ds-adaptive
import { AdaptiveProvider, withLayoutType, useAdaptiveBootstrap, useAdaptiveLayout, isMobileLayout, LAYOUT_TYPE } from '@cloud-ru/ds-adaptive'

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

AdaptiveProvider в корне

AdaptiveProvider в корнеПровайдер раздаёт `layoutType`; вложенный компонент читает его через `useAdaptiveLayout()` и `isMobileLayout()`.
Десктопная ветка

useAdaptiveLayout(): desktop

tsx
import { AdaptiveProvider, isMobileLayout, LAYOUT_TYPE, LayoutType, useAdaptiveLayout } from '@cloud-ru/ds-adaptive';
import { SegmentControl } from '@cloud-ru/ds-segment-control';
import { Tag } from '@cloud-ru/ds-tag';
import { Typography } from '@cloud-ru/ds-typography';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';
import { useState } from 'react';

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

// Потребитель берёт раскладку из AdaptiveProvider через useAdaptiveLayout() — без пропа и обёрток.
// Так же ведут себя Adaptive*-компоненты внутри (на mobile уходят в BottomSheet).
function LayoutSurface() {
  const { layoutType } = useAdaptiveLayout();
  const mobile = isMobileLayout(layoutType);

  return (
    <Flex gap='2m' align='center' wrap>
      <Tag
        appearance={mobile ? 'blue' : 'green'}
        label={mobile ? 'Мобильная ветка → BottomSheet' : 'Десктопная ветка'}
      />
      <Typography variant='body' size='s'>
        useAdaptiveLayout(): {layoutType}
      </Typography>
    </Flex>
  );
}

export function ProviderBasic() {
  const [layoutType, setLayoutType] = useState<LayoutType>(LAYOUT_TYPE.Desktop);

  return (
    <AdaptiveProvider layoutType={layoutType}>
      <Flex direction='column' gap='2m' align='flex-start'>
        <SegmentControl
          items={LAYOUT_ITEMS}
          value={layoutType}
          onChange={value => setLayoutType(value as LayoutType)}
        />
        <LayoutSurface />
      </Flex>
    </AdaptiveProvider>
  );
}

Форс раскладки в поддереве

Форс раскладки в поддеревеВложенный `<AdaptiveProvider layoutType=“desktop”>` (или HOC `withLayoutType`) фиксирует раскладку для поддерева, затеняя внешний контекст.

из контекста:

mobile

форс поддерева на desktop:

desktop
tsx
import { AdaptiveProvider, isMobileLayout, LAYOUT_TYPE, LayoutType, useAdaptiveLayout } from '@cloud-ru/ds-adaptive';
import { SegmentControl } from '@cloud-ru/ds-segment-control';
import { Tag } from '@cloud-ru/ds-tag';
import { Typography } from '@cloud-ru/ds-typography';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';
import { useState } from 'react';

const CONTEXT_ITEMS = [
  { value: LAYOUT_TYPE.Mobile, label: 'mobile' },
  { value: LAYOUT_TYPE.Desktop, label: 'desktop' },
];

// Раскладка из общего контекста (AdaptiveProvider выше) — следует за переключателем.
function FromContext() {
  const { layoutType } = useAdaptiveLayout();

  return (
    <Flex gap='1m' align='center'>
      <Typography variant='body' size='s'>
        из контекста:
      </Typography>
      <Tag appearance={isMobileLayout(layoutType) ? 'blue' : 'green'} label={layoutType} />
    </Flex>
  );
}

// Форс раскладки в поддереве — вложенный `<AdaptiveProvider layoutType='desktop'>` затеняет внешний
// контекст (то же делает HOC `withLayoutType(Component, 'desktop')` на module-scope). Эта ветка
// остаётся desktop при любом значении переключателя — пропа `layoutType` у компонента нет.
function ForcedDesktop() {
  const { layoutType } = useAdaptiveLayout();

  return (
    <Flex gap='1m' align='center'>
      <Typography variant='body' size='s'>
        форс поддерева на desktop:
      </Typography>
      <Tag appearance='green' label={layoutType} />
    </Flex>
  );
}

export function LayoutTypeOverride() {
  const [context, setContext] = useState<LayoutType>(LAYOUT_TYPE.Mobile);

  return (
    <AdaptiveProvider layoutType={context}>
      <Flex direction='column' gap='2m' align='flex-start'>
        <SegmentControl items={CONTEXT_ITEMS} value={context} onChange={value => setContext(value as LayoutType)} />
        <FromContext />
        <AdaptiveProvider layoutType={LAYOUT_TYPE.Desktop}>
          <ForcedDesktop />
        </AdaptiveProvider>
      </Flex>
    </AdaptiveProvider>
  );
}

Свои брейкпоинты приложения

Свои брейкпоинты приложения`useAdaptiveBootstrap({ breakpoints })` переопределяет пороги раскладки; набор тиров остаётся прежним.

Порог mobile опущен до 480 px. Сузьте окно до этой ширины, чтобы раскладка стала мобильной.

layoutType: desktop
tsx
import { AdaptiveProvider, isMobileLayout, useAdaptiveBootstrap, useAdaptiveLayout } from '@cloud-ru/ds-adaptive';
import { Tag } from '@cloud-ru/ds-tag';
import { Typography } from '@cloud-ru/ds-typography';
import { Flex } from '@cloud-ru/ds-uikit-product-flex';

// Раскладка приходит из контекста; Tag перекрашивается, когда ширина окна пересекает порог.
function LayoutSurface() {
  const { layoutType } = useAdaptiveLayout();
  const mobile = isMobileLayout(layoutType);

  return <Tag appearance={mobile ? 'blue' : 'green'} label={`layoutType: ${layoutType}`} />;
}

export function CustomBreakpoints() {
  // Брейкпоинты переопределяются на уровне приложения: mobile-порог опущен с 767 до 480 px.
  // useAdaptiveBootstrap() читает ширину окна в корне приложения и передаёт результат в AdaptiveProvider.
  const { layoutType } = useAdaptiveBootstrap({ breakpoints: { mobile: 480 } });

  return (
    <AdaptiveProvider layoutType={layoutType}>
      <Flex direction='column' gap='2m' align='flex-start'>
        <Typography variant='body' size='s'>
          Порог mobile опущен до 480 px. Сузьте окно до этой ширины, чтобы раскладка стала мобильной.
        </Typography>
        <LayoutSurface />
      </Flex>
    </AdaptiveProvider>
  );
}

Раскладки и брейкпоинты

layoutType выбирается по ширине окна (max-width в px). Дефолтные пороги:

layoutTypeПорогНазначение
mobile≤ 767 pxТелефон — единственный тир, уходящий в mobile-ветку (BottomSheet).
tablet≤ 1023 pxПланшет — десктопная ветка.
desktopSmall≤ 1279 pxУзкий десктоп — десктопная ветка.
desktop≤ 1439 pxДесктоп — десктопная ветка. SSR-baseline.

Ширина ≥ 1440 px (large) достижима только через useAdaptiveMatchMedia() — в layoutType она не маппится. Пороги переопределяются на уровне приложения через useAdaptiveBootstrap({ breakpoints }).

На сервере и до монтирования раскладка равна desktop (DEFAULT_LAYOUT_TYPE) — это baseline без доступа к вьюпорту, а не «всегда desktop». На сервере её можно выбрать по request-User-Agent (см. рецепт «Сервер» ниже). Зафиксировать раскладку для поддерева можно вложенным <AdaptiveProvider layoutType=…> или withLayoutType(...) — он затеняет внешний контекст.

Подписка на раскладку

Раскладку вычисляют один раз в корне и раздают через AdaptiveProvider; компоненты её только читают. Утилиты:

Реактивный корень (CSR). useAdaptiveBootstrap() вычисляет layoutType по user-agent + media-query и сам переподписывается на изменения вьюпорта (resize / поворот). Результат скармливается провайдеру:

import { AdaptiveProvider, useAdaptiveBootstrap } from '@cloud-ru/ds-adaptive'

function Root({ app }) {
  const { layoutType } = useAdaptiveBootstrap() // ре-вычисляется при смене ширины окна
  return <AdaptiveProvider layoutType={layoutType}>{app}</AdaptiveProvider>
}

Чтение в компоненте. useAdaptiveLayout() берёт раскладку из ближайшего AdaptiveProvider и ре-рендерит потребителя при её смене:

import { isMobileLayout, useAdaptiveLayout } from '@cloud-ru/ds-adaptive'

const { layoutType } = useAdaptiveLayout()
if (isMobileLayout(layoutType)) { /* mobile-ветка */ }

Произвольные media-query. Для условий вне layoutType (например, тир large ≥ 1440 px) — useAdaptiveMatchMedia().

Сервер (SSR по User-Agent). На сервере navigator недоступен, поэтому useAdaptiveBootstrap отдаёт desktop. Чтобы убрать flip при гидрации — резолвьте раскладку из request-UA через getAdaptive из серверобезопасного входа @cloud-ru/ds-adaptive/ssr (без React-импортов) и передайте статикой:

import { getAdaptive, INITIAL_ADAPTIVE_QUERIES_VALUE } from '@cloud-ru/ds-adaptive/ssr'

const { layoutType } = getAdaptive(INITIAL_ADAPTIVE_QUERIES_VALUE, headers().get('user-agent'))
// <AdaptiveProvider layoutType={layoutType}>{children}</AdaptiveProvider>

Props

Types

PropsAdaptiveProviderProps
PropTypeDefaultRequiredDescription
childrenstring | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | null | undefinedyes
layoutType"desktop" | "desktopSmall" | "mobile" | "tablet"noСтатичная раскладка (SSR — значение на запрос, либо `useAdaptiveBootstrap()` в корне CSR). Реактивный источник — через `store`.
storeAdaptiveStorenoВнешний реактивный стор раскладки; приоритетнее `layoutType`, обновляет подписчиков без перерендера провайдера.

Unions

Types

AdaptiveProviderProps

Unions

Смотри также