Миграция surface

Рецепты перевода существующего приложения-микрофронтенда с адаптивности на основе viewport на контракт surface.

Каждый рецепт помечен:

Метка Значение
автоматически Механическое преобразование. Преобразованное правило означает то же самое.
условно Безопасно только при выполнении названного предусловия. Проверьте его.
вручную Требуется решение человека; единственно верного переписывания нет.
не преобразуется Формы container query не существует. Используйте host.surface или намеренно сохраните поведение на основе viewport.

Каждый рецепт ниже — техника в отрыве от остальных. В репозитории Web Host есть работающая страница, объединяющая их все, выполняемая его набором тестов, чтобы рецепты не превратились в неверные инструкции.

Рецепты, зависящие от невыпущенной работы — варианты Tailwind surface-*, диагностика на этапе сборки, прокрутка через посредничество хоста, hit testing, — помечены ещё не выпущено и описывают только то, что существует сегодня.


Дерево решений: о чём это правило?

Прежде чем что-либо преобразовывать, классифицируйте намерение. Большинство плохих миграций — корректно выполненные преобразования правил, которые не следовало преобразовывать.

Реагирует ли правило на то, сколько места есть у ЭТОЙ СТРАНИЦЫ?
├── да → преобразовать в @container wippy-surface     (рецепты 1-8)
├── нет, оно реагирует на ширину одного КОМПОНЕНТА
│        → дайте этому компоненту свой container       (рецепт 22)
├── нет, оно реагирует на ПРЕДПОЧТЕНИЕ пользователя/устройства
│        → оставьте его как @media                     (рецепт 13)
└── нет, оно намеренно отслеживает ОКНО БРАУЗЕРА
         (настоящий полнооконный оверлей)
         → оставьте его и задокументируйте почему

Если понять не удаётся, оставьте как есть и вернитесь позже. Непреобразованный медиазапрос всего лишь непереносим; неверно преобразованный — молча сломан.


1. max-width → inline-size <= — автоматически

/* было */  @media (max-width: 640px)                      { .nav { display: none } }
/* стало */ @container wippy-surface (max-width: 640px)    { .nav { display: none } }

2. min-width → inline-size >= — автоматически

/* было */  @media (min-width: 640px)                      { .sidebar { display: block } }
/* стало */ @container wippy-surface (min-width: 640px)    { .sidebar { display: block } }

3. Ограниченный диапазон ширины — автоматически

/* было */  @media (min-width: 640px) and (max-width: 1024px) { … }
/* стало */ @container wippy-surface (640px <= width <= 1024px) { … }

Синтаксис диапазонов поддерживается всеми движками, на которые нацелен контракт surface. Форма с and тоже работает, если она вам привычнее.

4. Несколько брейкпойнтов с сохранением порядка каскада — автоматически

Container query не меняют специфичность или порядок. Преобразуйте каждый блок и сохраните тот же порядок в исходнике:

@container wippy-surface (min-width: 480px)  { .grid { grid-template-columns: repeat(2, 1fr) } }
@container wippy-surface (min-width: 900px)  { .grid { grid-template-columns: repeat(4, 1fr) } }

5. Запросы по высоте — условно (только при container sizing)

/* стало */ @container wippy-surface (min-height: 500px) { .tall-only { display: block } }

Предусловие: страница использует container sizing. При content sizing высота страницы — это её собственное содержимое, поэтому запросы по высоте никогда не срабатывают. Объявите зависимость, чтобы отказ был громким, а не молчаливым:

{ "wippy": { "surface": { "contract": 1, "requirements": ["block-size"] } } }

6. Запросы по aspect-ratio — условно (только при container sizing)

/* было */  @media (min-aspect-ratio: 16/9)                     { … }
/* стало */ @container wippy-surface (min-aspect-ratio: 16/9)   { … }

То же предусловие, что и в рецепте 5: соотношению сторон нужны обе оси.

7. Запросы по orientation — условно (только при container sizing)

@container wippy-surface (orientation: landscape) описывает форму вашей панели, а это обычно и имелось в виду. Если вы действительно имели в виду устройство, это медиазапрос — сохраните его (рецепт 13).

8. Высота / соотношение сторон / ориентация при content sizing — не преобразуется

Блочной оси для запроса нет. Перестройте вёрстку так, чтобы она зависела от строчной оси. Не подделывайте это через cqh — см. рецепт 22.

Вы не можете самостоятельно переключить приложение на container sizing: способ определения размера задаётся тем, где Web Host отрисовывает приложение, а не чем-либо в его пакете. Если вёрстка действительно не может работать без блочной оси, объявите requirements: ["block-size"], чтобы размещение с content sizing было отвергнуто сразу, а не отрисовано неверно, и добейтесь того, чтобы приложение отрисовывалось в контексте с container sizing (собственный маршрут или панель раскладки). См. «Container sizing и content sizing» в Переносимости surface.

9. Геометрия, вложенная в средовой медиазапрос — вручную

/* было */
@media (prefers-color-scheme: dark) and (min-width: 640px) { .panel { … } }

/* стало — разделите: предпочтение остаётся, геометрия переезжает */
@media (prefers-color-scheme: dark) {
  @container wippy-surface (min-width: 640px) { .panel { … } }
}

Вручную, потому что порядок вложенности может изменить, какие объявления побеждают, когда два условия ранее объединялись в одном прелюде. Перепроверьте результат.

10. Ветви через запятую (ИЛИ) — вручную

/* было */ @media (max-width: 480px), (min-width: 1200px) { … }

Запятая — это ИЛИ. Разделение на два блока @container сохраняет ИЛИ только если эти два блока в остальном идентичны и соседствуют; если вы случайно вложите их друг в друга, вы превратите ИЛИ в И, что не совпадёт ни с чем. Продублируйте объявления в два соседних блока:

@container wippy-surface (max-width: 480px)  { … }
@container wippy-surface (min-width: 1200px) { … }

11. not, only, сложная булева логика — вручную

only — артефакт типов медиа и не имеет эквивалента для container: уберите его. not инвертирует всё условие в обоих синтаксисах, но приоритет различается, как только вы смешиваете and/or; ставьте скобки явно, а не доверяйте исходной группировке.

12. screen / print в сочетании с геометрией — вручную

Типы медиа не имеют формы для container. Сохраните тип как медиазапрос и вложите геометрию внутрь него (как в рецепте 9). В частности, вёрстка для печати обычно должна целиком оставаться основанной на viewport/странице.

13. Предпочтения остаются медиазапросами — не преобразуется (и это правильно как есть)

prefers-color-scheme, prefers-contrast, prefers-reduced-motion, forced-colors, hover, pointer, any-pointer. @container поддерживает только размерные признаки. Преобразование этих запросов даёт правило, которое никогда не срабатывает.

14. Брейкпойнты в em — вручную

@media (min-width: 40em) разрешает em относительно начального размера шрифта. @container wippy-surface (min-width: 40em) разрешает его относительно размера шрифта контейнера. Если они различаются, ваш брейкпойнт молча сдвигается. Преобразуйте в px или сначала проверьте вычисленный font-size контейнера.

15. Брейкпойнты в rem — вручную

rem не относителен корню внутри @media. Условия медиазапросов разрешают и em, и rem относительно начального размера шрифта — умолчания браузера, не зависящего от авторского CSS, — тогда как @container разрешает их обычным способом, относительно фактического вычисленного размера шрифта корня/контейнера.

Поэтому эти два значения уже неравны в тот момент, когда корневой размер шрифта отличается от браузерного умолчания, и во время выполнения ничего не меняется. Распространённого сброса html { font-size: 62.5% } достаточно, чтобы сдвинуть преобразованный брейкпойнт с 640px на 400px.

Поэтому «ничто не меняет корневой размер шрифта» — не достаточное предусловие. Преобразуйте в px ровно так же, как для em (рецепт 14), если только вычисленный размер шрифта корня доказуемо не равен браузерному умолчанию.

16. Граница viewport и content-box при наличии полосы прокрутки — условно

100vw включает классический жёлоб полосы прокрутки. В движке iframe ширина surface — это content box блока запроса внутри документа приложения, поэтому она его не включает: на странице с полосой прокрутки документа преобразованное значение уже на ширину полосы прокрутки, и обычно это и есть нужная поправка (100vw, вызывающая горизонтальное переполнение, — классическая ошибка).

Движок фрагментов измеряет обёртку в документе хоста, которую прокрутка содержимого не сужает, поэтому эта поправка не применяется. Та же панель, то же прокручиваемое содержимое, ширины различаются на полосу прокрутки. Условие этого рецепта, таким образом, — в каком движке работает приложение, а не просто попиксельная точность выравнивания.

17. Правила, нацеленные на html / body — вручную

Container query никогда не стилизует собственный контейнер, и правило, нацеленное на html или body, не работает в обоих движках — по разным причинам:

  • Движок iframe: хост оборачивает содержимое вашего body в блок surface, поэтому html и body — предки контейнера запроса. Правило @container не может достать предка.
  • Движок фрагментов: обратная топология — блок запроса является обёрткой в документе хоста над вашим содержимым, — но буквальный селектор body всё равно не срабатывает, потому что отражённый документ переименован в wf-html / wf-body.

В любом случае исправление одно и то же, и оно безопасно для обоих движков:

/* ✗ молча никогда не срабатывает */
@container wippy-surface (min-width: 640px) { body { display: flex } }

/* ✓ перенесите на собственный корень внутри surface */
@container wippy-surface (min-width: 640px) { #app { display: flex } }

Выбор ресурсов на уровне HTML не имеет формы container query. Либо управляйте им из JS через host.surface.onChange, либо перенесите арт-дирекшн в CSS (background-image внутри правила @container), где контракт действует.

19. Геометрический matchMedia() → host.surface — автоматически

// было
const mq = matchMedia('(min-width: 640px)')
mq.addEventListener('change', render)

// стало
const off = host.surface.onChange(s => render(s.width >= 640))
render(host.surface.snapshot.width >= 640)
// вызовите off() при демонтировании

Сохраните matchMedia для запросов предпочтений — неверна только геометрия.

20. CSS во время выполнения, adopted stylesheets, CSS-in-JS — вручную

Предпочитайте выдавать правила @container wippy-surface (...) и позволять реагировать CSS. Если вы вычисляете пиксели в JS, пересчитывайте их из onChange — значение, однажды прочитанное из snapshot, заморожено и рассинхронизируется при следующем изменении размера. Никогда не выдавайте четыре зарезервированных имени --wippy-surface-* самостоятельно и никогда не регистрируйте их через @property / CSS.registerProperty() — регистрация ломает сигнал хоста «блочная ось недоступна», поэтому приложение с content sizing молча сообщает о себе как об использующем container sizing; объявление у потомка затеняет унаследованное значение и отвязывает вашу страницу от surface.

21. Сторонний CSS из бандла — вручную

Обычно вы не можете его править. В порядке предпочтения: настройте библиотеку так, чтобы она принимала брейкпойнт/ширину, которые вы передаёте из host.surface; оберните её в собственный контейнер и транслируйте; либо закрепите страницу за движком iframe (wippy.renderEngine: "iframe") и примите поведение на основе окна. Сканирование на этапе сборки для автоматического поиска таких случаев ещё не выпущено.

22. Вложенные контейнеры и ловушка отката cq* — вручную

Единицы контейнера разрешаются относительно ближайшего контейнера, у которого есть нужная им ось. Отсюда два следствия:

.card { container-type: inline-size; }   /* блочной оси НЕТ */
.card .thing { block-size: 25cqh; }      /* ✗ молча использует small viewport */

cqh/cqb не выдают ошибку, когда контейнер с блочной осью не найден: они откатываются к small viewport и отрисовывают правдоподобно неверное число. Используйте var(--wippy-surface-height, <fallback>), когда вам нужна блочная ось surface: она привязана к корню, поэтому более близкий контейнер не может её перехватить, и она заметно откатывается, когда недоступна.

Запросы по компонентам дополняют, а не заменяют: wippy-surface по-прежнему обозначает область страницы даже изнутри вложенного контейнера.


Единицы viewport

Было Используйте Примечания
100vw var(--wippy-surface-width) content box; см. рецепт 16
1vw / 37vw calc(var(--wippy-surface-width-unit) * 37) или 37cqw единица равна 1%
100vh var(--wippy-surface-height) только при container sizing
1vh / 37vh calc(var(--wippy-surface-height-unit) * 37) только при container sizing
vmin min(var(--wippy-surface-width), var(--wippy-surface-height)) только при container sizing — нужны обе оси
vmax max(var(--wippy-surface-width), var(--wippy-surface-height)) только при container sizing
vi / vb cqi / cqb либо физические переменные логические; переменные surface физические
sv* / lv* / dv* var(--wippy-surface-*) отдельных эквивалентов нет. Они описывают состояния браузерного обрамления, которых у панели нет; у surface один размер

sv*/lv* — реальные единицы CSS, они не означают «surface».

Вычисления

/* было */  block-size: calc(100vh - 4rem);
/* стало */ block-size: calc(var(--wippy-surface-height, 400px) - 4rem);

Значение по умолчанию намеренно фиксировано и очевидно неверно, а не равно 100vh, — см. «Не прячьте отсутствующий контракт за значением по умолчанию» ниже. На блочной оси это важнее, чем на строчной: высота невалидна при каждом размещении с content sizing, а не только там, где контракт отсутствует, поэтому запасное 100vh молча отрисует высоту окна при первом же встраивании приложения.

min()/max()/clamp() преобразуются без изменений; подставьте единицы внутри них.

Когда 100% лучше значения surface

Если элемент должен заполнить родителя, используйте 100% или w-full. Обращайтесь к --wippy-surface-width только когда вам нужна именно область страницы — обычно потому, что предок уже и вы хотите из него выйти. Привязка к корню того, что должно быть относительным к родителю, — верный способ получить вёрстку, корректную на одной глубине вложенности и неверную на другой.

Не прячьте отсутствующий контракт за значением по умолчанию

/* ✗ */ inline-size: var(--wippy-surface-width, 100vw);

Это отрисует ширину окна при отсутствии контракта — ровно ту ошибку, ради предотвращения которой контракт существует, только сделанную невидимой. Пусть она проявится явно, либо выберите фиксированное значение по умолчанию, очевидно неверное (400px), чтобы его заметили.


Оверлеи

Контракт surface не захватывает position: fixed: container-type создаёт независимый контекст форматирования без layout containment, поэтому контейнер запроса вычисляет contain: none и ничего не якорит. Это проверено в Chromium, Firefox и WebKit. И оверлеи PrimeVue, и самодельные fixed-оверлеи продолжают работать, поэтому позиционирование не требует миграции.

А вот их размеры требуют. Оверлей, предназначенный покрыть surface, должен использовать inset: 0, а не 100vw/100vh, которые измеряют окно браузера и выходят за пределы в многопанельном хосте, и не var(--wippy-surface-height), недоступный при content sizing. Сочетайте inset: 0 с position: absolute внутри собственного корня приложения с position: relative, если оверлей должен работать в обоих движках; position: fixed корректен только в движке iframe по причине, указанной ниже.

Внимания требует именно движок, а не контракт: в движке Web Fragment position: fixed разрешается относительно окна хоста, а не вашей панели. См. Движки отрисовки и закрепите приложение через wippy.renderEngine: "iframe", если это важно.

Размещение оверлеев через посредничество хоста и вспомогательные функции прокрутки host.surface ещё не выпущены.


Чек-лист

  1. Классифицируйте каждое правило (страница / компонент / предпочтение / намеренное окно).
  2. Преобразуйте геометрию с намерением «страница» в @container wippy-surface.
  3. Замените единицы viewport переменными surface.
  4. Перенесите любое правило, нацеленное на html/body, на собственный корневой элемент.
  5. Перепроверьте брейкпойнты в em.
  6. Объявите requirements, если вы зависите от блочной оси.
  7. Запустите страницу в обоих движках и при обоих способах определения размера — container и content и есть то, что эта миграция на самом деле затрагивает, а приложение использует content sizing всякий раз, когда оно встроено, а не открыто по маршруту. Проверьте, в каком вы режиме, через host.surface.snapshot.sizing, и ставьте поведение, зависящее от блочной оси, под условие host.surface.supports('block-size').