Перейти к содержимому

grid сетка —— Макет

Сетка — это каркас раскладки: страница режется на 24 колонки, содержимое располагается пропорционально ширине колонок и адаптируется по брейкпоинтам. Сетка lh-ui построена на CSS Grid (не на процентных ширинах flex) — выше точность позиционирования, поддерживается явное задание начальной точки. core/grid отвечает за определение брейкпоинтов, адаптивный откат и клампинг-расчёт расположения колонок — сам CSS не умеет ни «откатываться вниз», ни «клампить при выходе за границы».

  • Реализация <LhRow> / <LhCol> на всех концах: основной каркас страницы, двух-/трёхколоночные раскладки форм, карточная стена, дашборды.
  • Нужны адаптивные ширины колонок (на ПК три колонки, на планшете две, на телефоне одна).
  • Не следует использовать: простое выравнивание двух элементов (flex или space подойдут напрямую); поточная раскладка с нефиксированной шириной (фиксированные пропорции колонок grid будут только мешать).
import {
LH_GRID_COLUMNS,
LH_GRID_BREAKPOINTS,
LH_GRID_BREAKPOINT_ORDER,
resolveBreakpoint,
resolveResponsiveSize,
getGridRowClassNames,
normalizeColSize,
clampSpan,
getColStyle,
getGridColClassNames,
} from '@lh-ui/share/core/grid'
import type {
LhGridBreakpoint, LhGridSpan, LhColSize, LhBreakpointSizes,
} from '@lh-ui/share/core/grid'

CSS Grid, а не процентный flex: в flex-сетке каждая колонка имеет width: x/24, переносы строк делаются через flex-wrap, а межколоночные отступы компенсируются отрицательным margin — набор хаков. CSS Grid напрямую задаёт grid-template-columns: repeat(24, 1fr), отступы через gap, колонки располагаются через grid-column — семантика чистая. Расплата — отсутствие поддержки в IE, но все целевые концы lh-ui (современные браузеры + webview мини-программ) уже покрыты.

Откат брейкпоинта вниз: resolveResponsiveSize(sizes, 'md') идёт от ступени md в сторону уменьшения и берёт первую объявленную ступень. Если пользователь указал только { xs: 24, lg: 12 }, при ширине md (768~1024) произойдёт откат к xs, то есть 24 — не undefined. Это ключевой опыт адаптивного API: пользователь объявляет только ключевые брейкпоинты, а промежуточные ступени автоматически наследуют конфигурацию для меньших экранов.

span = 0 — это сигнальное значение, а не баг: span: 0, offset: 6 означает «не занимать колонки, только сдвинуть последующее содержимое на 6 колонок». При этом getColStyle выведет 7 / 25 (с продлением до последней колонки), а не 7 / span 0 — последнее схлопнуло бы элемент в нулевую ширину.

Клампинг начальной точки защищает от разрыва контейнера: offset 21 + span 8 даёт начальную точку 22 — за пределами сетки. getColStyle клампит старт в 24 + 1 - 8 = 17 — элемент целиком сдвигается влево до тех пор, пока не поместится. В комментарии указана причина: вышедший за границы grid-column заставляет Grid создать неявный трек и разорвать ширину контейнера.

Раздел между авто-размещением и явным позиционированием: чистый span использует span N (без указания точки старта, Grid размещает автоматически); наличие offset/push/pull — start / span N (явная начальная точка, прерывающая авто-размещение). В комментарии зафиксирована реальная ловушка: если чистый span записать как 1 / span N, все Col в строке начнутся с колонки 1, и Grid вытолкнет остальные на следующую строку — визуально получится «вертикальный столбец».

Таблица имён классов вместо прямой конкатенации: если бы justify="center" и align="center" склеивали имена напрямую, получился бы дубль lh-row--center (justify работает через justify-items, align — через align-items, это два разных CSS-свойства). С явной таблицей отображения: center для justify → lh-row--center, center для align → lh-row--middle (center как совместимый алиас middle, в духе привычных API популярных библиотек). Побочный плюс: неизвестные значения не порождают грязные имена классов.

Функция / константа Сигнатура Возврат Назначение
resolveBreakpoint (width: number) => LhGridBreakpoint брейкпоинт ширина вьюпорта → ступень
resolveResponsiveSize (sizes, breakpoint) => LhColSize | undefined действующий размер откат вниз до ближайшей объявленной ступени
getGridRowClassNames (state: { justify?, align?, wrap? }) => string[] классы выравнивание/перенос
normalizeColSize (size?: LhColSize) => LhGridSpan объект-описание число → { span }
clampSpan (span?: number) => number целое в [0, 24] неконечные значения → 0
getColStyle (size?: LhColSize) => Record<string, string> инлайновые стили расположение grid-column
getGridColClassNames (size?: LhColSize) => string[] классы span / offset

Константы: LH_GRID_COLUMNS = 24, LH_GRID_BREAKPOINTS (xs 0 / sm 640 / md 768 / lg 1024 / xl 1280), LH_GRID_BREAKPOINT_ORDER (от большего к меньшему).

export type LhGridBreakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export interface LhGridSpan {
span?: number // количество занимаемых колонок (0 = не занимать, используется только для позиционирования offset)
offset?: number // количество пустых колонок слева
push?: number // сдвиг начальной точки вправо
pull?: number // сдвиг начальной точки влево
}
export type LhColSize = number | LhGridSpan
export type LhBreakpointSizes = Partial<Record<LhGridBreakpoint, LhColSize>>

Сетка — это примитив чистой раскладки, не имеющий собственной семантики доступности — не добавляйте Row/Col роль. Семантику содержимого предоставляют внутренние компоненты. Один сопутствующий принцип:

  • Изменение раскладки не должно менять порядок DOM: при адаптивном переупорядочивании порядок Col в DOM фиксирован, меняется только визуальное количество колонок. Если на разных брейкпоинтах требуется разный порядок содержимого («основной контент на ПК слева, на мобильном вверху»), используйте порядок в исходном коде + стиль order, а не переключайте порядок DOM — скринридер зачитывает в порядке DOM, и несовпадение визуального порядка с порядком озвучивания вызовет путаницу.
import {
resolveBreakpoint, resolveResponsiveSize, getColStyle, clampSpan,
} from '@lh-ui/share/core/grid'
resolveBreakpoint(800) // 'md'
resolveBreakpoint(600) // 'xs' (< 640)
resolveBreakpoint(1300) // 'xl'
// Откат вниз: ступень md не объявлена → берётся xs
resolveResponsiveSize({ xs: 24, lg: 12 }, 'md') // 24
resolveResponsiveSize({ xs: 24, lg: 12 }, 'lg') // 12
resolveResponsiveSize({ xs: 24 }, 'xl') // 24
resolveResponsiveSize(undefined, 'md') // undefined (нет объявлений)
getColStyle(8) // { gridColumn: 'span 8' } (авто-размещение)
getColStyle({ span: 8, offset: 4 })
// { gridColumn: '5 / span 8' } (старт = offset+1, после клампинга границ не нарушены)
getColStyle({ span: 0, offset: 6 })
// { gridColumn: '7 / 25' } (сигнальное значение: Colon не занимаются, продление до последней)
getColStyle({ span: 8, offset: 21 })
// { gridColumn: '17 / span 8' } (клампинг при выходе за границу: старт 22 → 17)
getColStyle({ span: 8, push: 4 })
// { gridColumn: '5 / span 8' } (push 4 = сдвиг начальной точки вправо на 4)
getColStyle({ span: 8, pull: 4 })
// { gridColumn: '1 / span 8' } (старт 5-4=1, нижняя граница клампинга 1)
getColStyle({}) // { gridColumn: 'span 24' } (без объявлений — на всю ширину)
clampSpan(8) // 8
clampSpan(30) // 24 (кламп до верхнего предела)
clampSpan(-5) // 0
clampSpan(NaN) // 0 (неконечное значение → 0)
<!-- Vue3: адаптивные три колонки → две → одна -->
<template>
<LhRow :gutter="16">
<LhCol v-for="item in list" :key="item.id" :xs="24" :sm="12" :lg="8">
<LhCard>{{ item.title }}</LhCard>
</LhCol>
</LhRow>
</template>
<!-- Двухколоночная раскладка формы + кнопка с позиционированием offset -->
<template>
<LhRow :gutter="20">
<LhCol :span="12"><LhFormItem label="Имя" prop="name" /></LhCol>
<LhCol :span="12"><LhFormItem label="Эл. почта" prop="email" /></LhCol>
<LhCol :span="12" :offset="12">
<LhButton type="primary" @click="submit">Отправить</LhButton>
</LhCol>
</LhRow>
</template>
// React: слежение за брейкпоинтами ведёт UI-слой через resize-листенер или useMediaQuery,
// управляющий state; getColStyle вызывается как чистая функция в render
// uniapp: <view> + grid; на стороне мини-программ нет window, брейкпоинт
// определяется UI-слоем по safeArea/windowWidth из uni.getSystemInfoSync()
// или пересчётом res.rpx, а resolveBreakpoint из core используется как есть
Класс Семантика
.lh-row контейнер строки (display: grid; grid-template-columns: repeat(24, 1fr))
.lh-row--{end|center|space-between|space-around} горизонтальное выравнивание (justify-items)
.lh-row--{middle|bottom} вертикальное выравнивание (align-items)
.lh-row--nowrap без переноса
.lh-col контейнер колонки
.lh-col--{1..24} количество колонок span
.lh-col--offset-{1..24} количество колонок смещения слева

Токен отступов --lh-row-gutter (междустрочный интервал, переопределяется prop gutter); промежутки между колонками реализуются через grid gap.

  • gutter и grid gap: gap в Grid действует одновременно на строки и колонки; если нужен только межколоночный отступ без межстрочного, контейнер строки нужно отдельно переопределить row-gap: 0.
  • За брейкпоинтами должен следить UI-слой: resolveBreakpoint — чистая функция, UI-слой должен сам слушать resize (или использовать matchMedia) и подавать ширину. При SSR на первом экране нет window, по умолчанию рендерится ступень xs, а после гидратации на клиенте значение исправляется.
  • Не смешивайте авто-размещение и явное позиционирование: если в одной строке один Col использует чистый span, а другой — offset, явно позиционированный прервёт авто-размещение — результат размещения в строке может оказаться неочевидным. Либо все с чистым span, либо все с явным позиционированием.
  • Разница между push / pull и offset: все три меняют начальную точку, но семантика разная — offset оставляет «пустое место слева» (семантика резерва), push/pull — «сдвиг» (визуальное переупорядочивание). Реализация в getColStyle всюду одна — сдвиг старта, на уровне имён классов соответствует только offset (push/pull идут только через инлайновые стили).
  • 24-колоночная система координат — жёсткое ограничение: span 25 будет заклампен к 24; offset 24 + span 1 — старт клампится к 24. Если бизнесу нужна более мелкая дискретность свыше 24, используйте вложенность LhRow, а не увеличивайте количество колонок.
  • Вложенные сетки: внутри LhCol снова размещается LhRow, и внутренняя сетка начинает отсчёт заново с 24 колонок — дочерняя сетка не наследует span-контекст родительской. Это естественное поведение CSS Grid.
  • Сосуществование класса span и инлайнового стиля: lh-col--8 из getGridColClassNames и grid-column из getColStyle — два механизма (классы для медиавыражений SCSS, инлайн для адаптива из JS). UI-слой выбирает один из двух: одновременная запись обоих приведёт к взаимному перекрытию (инлайновый стиль победит).
@lh-ui/share/core/grid → getColStyle( )
返回值
(填好参数后点运行)

Copyright © 2024-2026 FengMo WenXue