Сегодня мы продолжим переписывание на $mol этой демки. Кто не читал первую часть, рекомендую сначала ознакомиться с ней BALLSORT на $mol. Часть 1
Напомню задачу
Экраны
Start - стартовый экран на котором отображается заголовок, кнопка для запуска игры, и подвал с cсылками
Game - при клике на кнопку запуска, открывается экран с игрой, на котором необходимо сортировать шарики. В хедере находятся кнопки возврата на стартовый экран и рестарта игры, а также счетчик числа сделаyных шагов. В центре трубки с шарами. В подвале те же ссылки что и на первом экране.
Finish - когда шарики отсортированы, поверх второго экрана отображается третий экран. На нем находится заголовок "You won!", количество сделанных шагов, и кнопка "New game" которая открывает стартовый экран.
Механика игры
Рисуются 6 трубок, четыре и них заполнены шарами и две пустые
В заполненных трубках находятся по 4 шара, четырех разных цветов
-
При клике на непустую трубку, она переходит в активное состояние
В активном состоянии верхний шар в трубке переносится на ее крышку
Повторный клик по активной трубке дезактивирует ее, шар переносится обратно в нее
После активации трубки, клик по другой трубке переносит шар с крышки в другую трубку при условии, что другая трубка пуста или верхний шар другой трубки такого же цвета как шар на крышке активной трубке
Когда в одной и трубок все 4 шара одного цвета она переходит в статус готово, после этого шары в нее/из нее перемещать нельзя.
Игра закончится, когда 4 трубки перейдут в статус готово.
Отображение
Мы создадим отдельные модули для отображения:
Ссылки
Кнопки
Шара
Трубки
А затем соберем все в модуле app
.
link
Создайте директорию ballsort/link
и файл в ней link.view.tree
.
$hype_ballsort_link $mol_view
dom_name \a
attr *
href <= href \
target <= target \_self
sub / <= title \
view.tree - это DSL, прежде чем продолжать рекомендую ознакомиться с этими трудами: Композиция компонентов, Декларативная композиция компонентов
После того как вы ознакомились с материалами по ссылкам выше вы понимаете, что мы описали класс $hype_ballsort_link
, который наследуется от базового класса view-компонент $mol_view
. Имя тега изменено на a
, у dom-ноды установлены два аттрибута href
и target
на которые забиндены одноименные свойства, а в качестве ребенка dom-ноды выводим строку из свойства title
.
Отрисуем этот компонент. Откройте в браузере ссылку http://127.0.0.1:9080/hype/ballsort/app/-/test.html
- это модуль приложения, в котором находится файл index.html
. На экране отображается только строка приветствия.
Отредактируйте файл app/app.view.tree
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hype_ballsort_link
title \Ссылка
href \example.com
target \_blank
$mol_list - это view-компонент из мола, для отображения вертикального списка, временно воспользуемся им.
Заглянем в браузер:
Добавим стилей, создайте в link
файл link.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_link, {
color: 'lightgray',
padding: ['0.25rem', '1rem'],
} )
}
Про
css.ts
можно почитать тут: Каскадные стили компонент, Продвинутый CSS-in-TS, $mol_style readme.md
Готово, компонент ссылки теперь имеет необходимый функционал и выглядит также как в оригинальном приложении.
button
Проделываем все тоже самое для компонента кнопки.
Файл ballsort/button/button.view.tree
:
$hype_ballsort_button $mol_view
dom_name \button
sub / <= title \
event *
click? <=> click? null
Выводим кнопку в app
:
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hypr_ballsort_link
title \Ссылка
href \example.com
target \_blank
<= Button $hype_ballsort_button
title \Кнопка
Убеждаемся, что кнопка подтянулась:
Добавляем стили в файле button.view.css.ts
:
namespace $.$$ {
$mol_style_define( $hype_ballsort_button, {
width: 'fit-content',
backgroundColor: 'white',
color: 'black',
padding: ['0.6rem', '1rem'],
fontSize: '1.3rem',
margin: [0, '0.2rem'],
border: {
width: '2px',
style: 'solid',
color: 'lightgray',
},
cursor: 'pointer',
position: 'relative',
':hover': {
backgroundColor: '#f1f1f1',
},
':focus': {
outline: 'none',
boxShadow: '0 0 0 4px lightblue',
borderColor: 'lightblue',
},
} )
}
ball
Теперь создадим компонент для шара. Имя $hype_ballsort_ball
уже занято в классе модели, view-шку шара поместим в $hype_ballsort_ball_view
.
Создайте файл ballsort/ball/view/view.view.tree
Комментарии во view.tree начинаются со знака минус
$hype_ballsort_ball_view $mol_view
- Компонент шара будет принимать модель шара, из которой он достает цвет
ball $hype_ballsort_ball
- Для раскраски шара будет использоваться радиальный градиент из двух цветов
style *
--main-color <= color_main \
--light-color <= color_light \
- Цвета заранее заготовлены в массиве, такие же как в оригинальном приложении
- Всего предусмотрено 12 цветов, индексы от 0 до 11
- цвет по индексу 0 - основной цвет - color_main
- цвет по индексу 0 + 1 - второй цвет - color_light
colors /
\#8F7E22
\#FFE600
\#247516
\#70FF00
\#466799
\#00B2FF
\#29777C
\#00FFF0
\#17206F
\#4A72FF
\#BABABA
\#FFFFFF
\#4C3283
\#9D50FF
\#8B11C5
\#FF00F5
\#9D0D41
\#FF60B5
\#4B0000
\#FF0000
\#79480F
\#FF7A00
\#343434
\#B1B1B1
Рисуем шар в app
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hype_ballsort_link
title \Ссылка
href \example.com
target \_blank
<= Button $hype_ballsort_button
title \Кнопка
<= Ball $hype_ballsort_ball_view
И не видем его, но он есть
Добавим ему стилей, создайте файл ball/view/view.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_ball_view, {
width: '2rem',
height: '2rem',
boxSizing: 'content-box',
border: {
radius: '50%',
width: '2px',
style: 'solid',
color: 'black',
},
margin: '1px',
position: 'relative',
backgroundImage: 'radial-gradient(circle at 65% 15%, white 1px, var(--light-color) 3%, var(--main-color) 60%, var(--light-color) 100%)',
} )
}
Теперь нужно научить шар брать нужные цвета, добавим логики. Создайте файл view.view.ts
.
namespace $.$$ {
export class $hype_ballsort_ball_view extends $.$hype_ballsort_ball_view {
// В свойстве ball хранится инстанс модели шара
// из модели достаем цвет `color()` и умножаем на 2
// чтобы получить правильный индекс в массиве цветов
color_index() {
return this.ball().color() * 2
}
// Достаем из массива основной цвет по посчитанному индексу
// На случай если нам пришел индекс выходящий за массив с цветами
// выводим красный цвет
color_main() {
return this.colors()[ this.color_index() ] ?? 'red'
}
// Достаем второй цвет по индексу + 1
// и устанавливаем значение по умолчанию
color_light() {
return this.colors()[ this.color_index() + 1 ] ?? 'white'
}
}
}
И т.к. в модели по дефолту стоит цвет 0, видим первый цвет из массива
tube
Нам осталось создать компонент для трубки. По аналогии с шаром, создайте файл tube/view/view.view.tree
Сделаем его на основе $mol_list
, т.к. он состоит из двух вертикальных частей
крышка
сама трубка с шариками, которая тоже на
$mol_list
$hype_ballsort_tube_view $mol_list
tube $hype_ballsort_tube
active false
event *
click? <=> click? null
rows /
<= Roof $mol_view sub / <= roof null
<= Balls $mol_list
style * min-height \10rem
attr *
data-complete <= complete false
rows <= balls /
<= Ball*0 $hype_ballsort_ball_view
ball <= ball* $hype_ballsort_ball
tube $hype_ballsort_tube
- также, как и компонент шара, у него будет хранится модель трубкиactive false
- свойство с типомboolean
нужно для отображения активацииevent * click? <=> click? null
- биндим свойствоclick
на событие кликаrows /
- для отображения детей у$mol_list
предусмотрено свойствоrows
, а неsub
как у$mol_view
<= Roof $mol_view sub / <= roof null
- в свойствеRoof
будет находится подкомпонент$mol_view
, который отображает содержимое свойстваroof
- оно по умолчаниюnull
. Но при активации трубкиroof
будет возвращать view-ку шара<= Balls $mol_list
- в свойствеBalls
подкомпонент на основе$mol_list
будет отображать шары в трубкеstyle * min-height \10rem
- минимальную высоту указываем черезstyle
attr * data-complete <= complete false
- чтобы отобразить состояние готово будем использовать data-аттрибутrows <= balls /
- у подкомпонентаBalls
свойствоrows
заменяем на наше свойствоballs
которое будет возвращать массив view-шек шаров
Про последнюю часть скажу отдельно.
<= Ball*0 $hype_ballsort_ball_view
ball <= ball* $hype_ballsort_ball
Свойство Ball
- это фабрика, которая в сгенерированном классе пометиться декоратором $mol_mem_key
. Т.е. она будет создавать и возвращать инстансы view-шек шаров точно также как мы делали это руками в $hype_ballsort_game
. Плюс к этому, у созданного инастана будет подменено свойство ball
на наше.
Пример из модели:
@$mol_mem_key
Tube( index: number ) {
const obj = new $hype_ballsort_tube
obj.size = () => this.tube_size()
return obj
}
А это будет сгенерировано из view.tree
описания:
@ $mol_mem_key
Ball(id: any) {
const obj = new this.$.$hype_ballsort_ball_view()
obj.ball = () => this.ball(id)
return obj
}
Выведим трубку в app
:
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hype_ballsort_link
title \Ссылка
href \example.com
target \_blank
<= Button $hype_ballsort_button
title \Кнопка
<= Ball $hype_ballsort_ball_view
<= Tube $hype_ballsort_tube_view
balls /
<= Ball1 $hype_ballsort_ball_view
color_index 2
<= Ball2 $hype_ballsort_ball_view
color_index 4
<= Ball3 $hype_ballsort_ball_view
color_index 6
И переопределим у нее свойство balls
чтобы увидеть несколько шаров. А чтобы у шаров были разные цвета, у каждого шара переопределим свойство color_index
.
Создайте файл tube/view/view.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_tube_view, {
// В оригинальном приложении box-sizing = content-box
// а у $mol_view по дефолту стоит border-box
// поэтому меняем
boxSizing: 'content-box',
width: 'fit-content',
Roof: {
boxSizing: 'content-box',
height: '3rem',
alignItems: 'center',
justifyContent: 'center',
border: {
bottom: {
style: 'solid',
color: 'lightgray',
},
},
},
Balls: {
boxSizing: 'content-box',
width: '3rem',
flex: {
direction: 'column-reverse',
},
justifyContent: 'flex-start',
alignItems: 'center',
border: {
width: '2px',
style: 'solid',
color: 'lightgray',
},
padding: {
bottom: '0.4rem',
top: '0.4rem',
},
borderRadius: '0 0 2.4rem 2.4rem',
'@': {
'data-complete': {
true: {
// Когда data-complete=true
backgroundColor: 'lightgray',
},
},
},
},
} )
}
В $hype_ballsort_app добавим трубке шар на крышку:
- ...
<= Tube $hype_ballsort_tube_view
balls /
<= Ball1 $hype_ballsort_ball_view
color_index 2
<= Ball2 $hype_ballsort_ball_view
color_index 4
<= Ball3 $hype_ballsort_ball_view
color_index 6
roof <= Ball4 $hype_ballsort_ball_view
color_index 8
Осталось добавить только поведение, создайте файл tube/view/view.view.ts
namespace $.$$ {
export class $hype_ballsort_tube_view extends $.$hype_ballsort_tube_view {
// Шар на крышке
@ $mol_mem
roof() {
// Получаем индекс последнего шара, напомню что this.tube() возвращает модель трубки
// Через фабрику получаем инстанс компонента шара который возвращаем
// Или возвращаем null
const index = this.tube().balls().length - 1
return this.active() ? this.Ball( index ) : null
}
// Массив компонентов шаров, которые будут отображаться в трубке
@ $mol_mem
balls() {
// В зависимости от активности трубки получаем список моделей шаров
const last_ball = this.tube().balls().at(-1)
const list = this.active() ? [last_ball] : this.tube().balls()
// Превращаем его в список компонентов шаров
return list.map((_, index) => this.Ball(index))
}
// Получаем модель шара по индексу
ball(index: number) {
return this.tube().balls()[index]
}
// Вытаскиваем из трубки состояние статуса готово
complete() {
return this.tube().complete()
}
}
}
title
Создадим подкомпонент для отображения заголовка.
Его не будем выносить в отдельный модуль. Добавим его как подкомпонент в app.view.tree
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
- ...
<= Title $mol_view
dom_name \h2
sub /
<= Title_begin $mol_view sub / \BALL
<= Title_end $mol_view sub / \SORT
Добавим ему стилей, создатйе файл app.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
Title: {
font: {
size: '3rem',
weight: 300,
},
},
Title_begin: {
textDecoration: 'underline',
},
} )
}
app
Теперь мы можем собрать экраны, удалим лишнее из app.view.tree
и создадим основную структуру:
$hype_ballsort_app $mol_view
game $hype_ballsort_game
title \BALL SORT
Title $mol_view
dom_name \h2
sub /
<= Title_begin $mol_view sub / \BALL
<= Title_end $mol_view sub / \SORT
sub /
<= Start_page $mol_list
<= Game_page $mol_list
<= Finish_page $mol_list
game $hype_ballsort_game
- в свойствеgame
будем хранить инстанс текущей игрыtitle \BALL SORT
- то что отобразится в заголовке вкладкиStart_page
,Game_page
,Finish_page
заготовки для страниц
Start_page
И давайте сразу оформим стартовый экран:
- ...
sub /
<= Start_page $mol_list
rows /
<= Title
<= Start $hype_ballsort_button
title \Start game
click? <=> start? null
<= Links $mol_view
sub /
<= Sources $hype_ballsort_link
title \Source Code
href \https://github.com/PavelZubkov/ballsort
target \_blank
<= Game_page $mol_list
<= Finish_page $mol_list
Первым у нас выводится
Title
Затем кнопка старта игры
Start
, клик по ней биндится на свойствоstart
, которому мы добавим поведение позжеИ выводится блок со ссылками в свойстве
Links
Посмотрим как это выглядит
Давайте добавим недостающие стили в app.view.css.ts
, я просто тащу их из оригинального приложения.
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol',
color: '#e1e1e1',
lineHeight: 'normal',
padding: {
top: '1rem',
},
justifyContent: 'center',
background: {
color: '#101526',
},
// Title, Title_begin ...
Links: {
padding: {
top: '1rem',
},
justifyContent: 'center',
flex: {
wrap: 'wrap',
},
},
Start_page: {
alignItems: 'center',
},
} )
}
Game_page
Перейдем к странице игры. Она состоит из трех вертикальных блоков
Кнопки управления + вывод количества шагов
Трубки с шариками
Те же ссылки что и на главной странице
Должно быть что-то такое:
Game_page
Control
Home - кнопка возврата на стартовый экран
Restart - кнопка перезапуска игры
Move - число с количеством шагов
Tubes - трубки с шариками
Links - ссылка
Добавим это в app.view.tree
$hype_ballsort_app $mol_view
- ...
sub /
<= Start_page $mol_list
- ...
<= Game_page $mol_list
rows /
<= Control $mol_view
sub /
<= Home $hype_ballsort_button
title \←
click? <=> home? null
<= Restart $hype_ballsort_button
title \Restart
click? <=> start?
<= Tubes $mol_view
<= Links
<= Finish_page $mol_list
Как мы будем определять запущена игра или нет?
Во view.tree у нас объявлено свойство game, которое хранит экземпляр класса игры. Во view.ts мы его переопределим, сделаем изменяемым свойством и по умолчанию оно будет возвращать null
. Логика такая:
game
возвращаемnull
- показываем стартовый экранgame
возвращает инстанс игры - показываем экран игрыКлик по кнопкам старт и рестарт будет помещать в свойство
game
новый экземпляр игрыКлик по кнопке назад будет помещать
null
в свойствоgame
Для понимания что игра закончена, в классе игры есть свойство
finish
будем использовать его
Как мы будем менять экраны?
Сейчас у нас все три экрана выведены в свойстве sub
. Во view.ts нам надо переопределить свойство sub
, чтобы оно в один момент времени возвращался только один, нужный экран.
Создайте файл app.view.ts
, помните про снипеты в VSCode, тут нужен снипет logic
.
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// Переопределяем свойство game
// Теперь оно изменяемое и nullable
@ $mol_mem
game(next?: $hype_ballsort_game | null) {
return next ?? null!
}
// Кнопки start и restart забиндены на свойство start
// Тут мы просто помещаем новый инстанс игры в свойство game
@ $mol_action
start() {
this.game( new $hype_ballsort_game )
}
// Кнопка возврата забиндена на свойство `home`
// Тут мы помещаем null в свойство game
@ $mol_action
home() {
this.game(null)
}
// Дети компонента $mol_view берутся из свойства sub
// Тут мы возвращаем нужный экран в зависимости состояния игры
@ $mol_mem
sub() {
if (!this.game()) return [ this.Start_page() ]
return [ this.game().finished() === false ? this.Game_page() : this.Finish_page() ]
}
}
}
Трубки и шары
Теперь пришла очередь отрисовать трубки с шарами. Нам надо взять список трубок из игры и вывести его обернув каждую модель трубки во view-компонент.
Изменим app.view.tree
$hype_ballsort_app $mol_view
- ...
sub /
<= Start_page $mol_list
- ...
<= Game_page $mol_list
rows /
<= Control $mol_view
- ...
<= Tubes $mol_view
sub <= tubes /
<= Tube*0 $hype_ballsort_tube_view
tube <= tube* $hype_ballsort_tube
click? <=> tube_click*? null
active <= tube_active* false
<= Links
<= Finish_page $mol_list
Что тут происходит:
<= Tubes $mol_view
- мы создаем подкомпонентTubes
на основе базового компонента$mol_view
и кладем его вrows /
у объекта в свойствеGame_page
sub <= tubes /
свойствоsub
уTubes
заменяем на свойствоtubes
и устанавливаем ему значение по умолчаниюА в качестве значение подставляем свойство-фабрику
Tube
на основе view-компонента трубки, и тут же настраиваем его подменяя свойстваtube
,click
,active
Код выше преобразуется в такой ts-код:
@ $mol_mem
Tubes() {
const obj = new this.$.$mol_view()
obj.sub = () => this.tubes()
return obj
}
tubes() {
return [
this.Tube("0")
] as readonly any[]
}
Нам надо переопределить tubes
, чтобы оно брало список трубок из модели игры и оборачивало во view-компонент трубки. Изменим app.view.ts
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// ...
@ $mol_mem
tubes() {
return this.game().tubes().map( ( _, index ) => this.Tube( index ) )
}
}
}
Добавим реализация для свойств tube
, tube_click
, tube_active
, которые мы описали во view.tree
tube <= tube* $hype_ballsort_tube
click? <=> tube_click*? null
active <= tube_active* false
Изменим app.view.ts
еще раз:
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// ...
@ $mol_mem
tubes() {
return this.game().tubes().map( ( _, index ) => this.Tube( index ) )
}
// По индексу достаем инстанс модели трубки из игры
// декротар тут можно опустить
tube( index: number ) {
return this.game().Tube(index)
}
// По клику вызываем tube_click в игре
// Передавая туда трубку по которой кликнули
@ $mol_action
tube_click( index: number ) {
this.game().tube_click( this.tube(index) )
}
// Проверяем активна ли текущая трубка
@ $mol_mem_key
tube_active( index: number ) {
return this.game().tube_active() === this.tube(index)
}
}
}
Давайте выведем количество шагов. Изменим app.view.tree
$hype_ballsort_app $mol_view
- ...
sub /
<= Start_page $mol_list
- ...
<= Game_page $mol_list
rows /
<= Control $mol_view
sub /
<= Home $hype_ballsort_button
title \←
click? <=> home? null
<= Restart $hype_ballsort_button
title \Restart
click? <=> start?
- Тут добавим Moves
<= Moves $mol_view
sub / <= moves \Moves: {count}
- ...
<= Finish_page $mol_list
А во view.ts переопределим свойство moves
- moves \Moves: {count}
, чтобы оно заменяло {count}
на число шагов
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// ...
@ $mol_mem
moves() {
return super.moves().replace( '{count}', `${ this.game().moves() }` )
}
}
}
И добавим стилей в app.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
// ...
Moves: {
padding: ['0.6rem', '0.4rem'],
fontSize: '1.3rem',
},
Tubes: {
justifyContent: 'center',
},
Control: {
justifyContent: 'center',
},
Tube: {
margin: '1rem',
},
} )
}
Finish_page
Осталось добавить только экран финиша. Изменим app.view.tree
:
$hype_ballsort_app $mol_view
- ...
sub /
- ...
<= Finish_page $mol_list
rows /
<= Control
<= Tubes
<= Links
<= Finish $mol_list
rows /
<= Finish_title $mol_view
dom_name \h1
sub / \You won!
<= Finish_moves $mol_view
dom_name \h2
sub / \In 16 moves
<= Finish_home $hype_ballsort_button
title \New game
click? <=> home?
Финишный экран, выводится поверх экрана игры. Мы также выводим Control
, Tubes
, Links
и после финишные надписи и кнопку.
Сразу добавим стилей для него в app.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
Finish: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
background: {
color: $mol_style_func.rgba(255, 255, 255, 0.6),
},
backdropFilter: $mol_style_func.blur('6px'),
alignItems: 'center',
paddingTop: '5rem',
},
Finish_title: {
color: 'black',
textShadow: '0 0 2px white',
},
Finish_moves: {
color: 'black',
textShadow: '0 0 2px white',
margin: {
top: '1rem',
},
},
Finish_home: {
margin: {
top: '1rem',
},
},
} )
}
Тестируем приложение
Напишем тест, чтобы убедится, что экраны у нас корректно меняются. Создайте файл app.view.test.ts
namespace $.$$ {
$mol_test({
"Screan changing"() {
const app = new $hype_ballsort_app
// По умолчанию должен показываться стартовый экран
$mol_assert_like(app.sub(), [app.Start_page()])
// Кликаем по кнопке старта и проверяем что теперь отображается экран игры
app.start()
$mol_assert_like(app.sub(), [app.Game_page()])
// Выиграем игру, просто установим всем шарам один цвет и проверим экран
app.game().balls().forEach(obj => obj.color(0))
$mol_assert_like(app.sub(), [app.Finish_page()])
},
})
}
Убедимся, что тест работает, сломав его, замените Finish_page
на Game_page
в последнем ассерте.
По всем вопросам можно идти сюда.
Комментарии (3)
FranCOder
12.06.2023 17:00Хватит отмечать хабы с другими фреймворками\библиотеками, Вам сделали хаб mol его только и выбирайте. Я не хочу видеть в ленте у себя этого франкенштейна
nin-jin
Стоит отметить, что в $mol нет необходимости велосипедить свои кнопки, ссылки и тд - можно взять стандартные $mol_link и $mol_button и настроить их вид по своему вкусу.
Комментарии лучше писать внутри узла данных, чтобы они не парсились в AST:
min-height
лучше не задавать руками, чтобы случайно не сломать работу виртуализации, так как он вычисляется автоматически, на основе контента.Вместо camelCase имён CSS-свойств лучше использовать строго-типизированные вложенные структуры: