[{"data":1,"prerenderedAt":1355},["ShallowReactive",2],{"docs:\u002Fdocs\u002Ffoundations\u002Fmobile":3},{"id":4,"title":5,"body":6,"component":1345,"dependsOn":1346,"description":1347,"emitDescriptions":1348,"extension":1283,"meta":1349,"navigation":217,"order":127,"path":1350,"seo":1351,"status":1352,"stem":1353,"__hash__":1354},"docs\u002Fdocs\u002Ffoundations\u002Fmobile.md","Мобільні пристрої",{"type":7,"value":8,"toc":1329},"minimark",[9,18,23,41,85,92,131,137,142,145,339,365,394,398,412,447,450,454,469,522,526,537,591,618,622,625,671,678,778,782,818,822,841,847,909,916,920,1055,1059,1072,1224,1247,1251,1307,1311,1325],[10,11,12,13,17],"p",{},"Компоненти розраховані на телефон так само, як на десктоп: мобільні розміри\nпишуться першими, а ",[14,15,16],"code",{},"md:"," повертає десктопну щільність. Нижче — контракт,\nна який можна покладатися, і одна річ, яку мусить зробити сам проєкт.",[19,20,22],"h2",{"id":21},"що-має-зробити-проєкт","Що має зробити проєкт",[10,24,25,32,33,36,37,40],{},[26,27,28,31],"strong",{},[14,29,30],{},"viewport-fit=cover"," у meta viewport."," Без нього iOS тримає сторінку\nпоза вирізом камери й заокругленими кутами, і ",[14,34,35],{},"env(safe-area-inset-*)","\nзавжди дорівнює нулю — тобто всі безпечні зони бібліотеки (футер модалки,\nнижній sheet, тостер, ",[14,38,39],{},"UiActionBar",", бічні панелі) просто не діють.",[42,43,48],"pre",{"className":44,"code":45,"language":46,"meta":47,"style":47},"language-html shiki shiki-themes github-light github-dark","\u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n","html","",[14,49,50],{"__ignoreMap":47},[51,52,55,59,63,67,70,74,77,79,82],"span",{"class":53,"line":54},"line",1,[51,56,58],{"class":57},"sVt8B","\u003C",[51,60,62],{"class":61},"s9eBZ","meta",[51,64,66],{"class":65},"sScJk"," name",[51,68,69],{"class":57},"=",[51,71,73],{"class":72},"sZZnC","\"viewport\"",[51,75,76],{"class":65}," content",[51,78,69],{"class":57},[51,80,81],{"class":72},"\"width=device-width, initial-scale=1, viewport-fit=cover\"",[51,83,84],{"class":57},">\n",[10,86,87,88,91],{},"Разом із цим сторінка заходить під виріз, тож власний хром проєкту має\nтримати бічні зони сам. Найпростіше — на ",[14,89,90],{},"body",":",[42,93,97],{"className":94,"code":95,"language":96,"meta":47,"style":47},"language-css shiki shiki-themes github-light github-dark","body {\n  padding-left: env(safe-area-inset-left);\n  padding-right: env(safe-area-inset-right);\n}\n","css",[14,98,99,106,116,125],{"__ignoreMap":47},[51,100,101,103],{"class":53,"line":54},[51,102,90],{"class":61},[51,104,105],{"class":57}," {\n",[51,107,109,113],{"class":53,"line":108},2,[51,110,112],{"class":111},"sj4cs","  padding-left",[51,114,115],{"class":57},": env(safe-area-inset-left);\n",[51,117,119,122],{"class":53,"line":118},3,[51,120,121],{"class":111},"  padding-right",[51,123,124],{"class":57},": env(safe-area-inset-right);\n",[51,126,128],{"class":53,"line":127},4,[51,129,130],{"class":57},"}\n",[10,132,133,134,136],{},"Фіксовані оверлеї бібліотеки рахують зони самостійно і від ",[14,135,90],{}," не\nзалежать.",[138,139,141],"h3",{"id":140},"нативне-відчуття-на-дотику","Нативне відчуття на дотику",[10,143,144],{},"Кілька рядків базового CSS проєкту прибирають ознаки «сайту» на телефоні:\nсірий спалах під пальцем, зум сторінки подвійним тапом і виділення тексту\nінтерфейсу довгим натисканням (воно конфліктує з контекстним меню).",[42,146,148],{"className":94,"code":147,"language":96,"meta":47,"style":47},"html {\n  -webkit-text-size-adjust: 100%;   \u002F* iOS у ландшафті не роздуває текст *\u002F\n  -webkit-tap-highlight-color: transparent;\n  touch-action: manipulation;       \u002F* без double-tap-zoom; щипок лишається *\u002F\n}\n\n@media (hover: none) and (pointer: coarse) {\n  body { -webkit-user-select: none; user-select: none; }\n  \u002F* iOS не дає друкувати в contenteditable під user-select: none *\u002F\n  input, textarea, [contenteditable='true'], .ui-selectable {\n    -webkit-user-select: text; user-select: text;\n  }\n}\n",[14,149,150,156,178,191,207,212,219,240,270,276,307,328,334],{"__ignoreMap":47},[51,151,152,154],{"class":53,"line":54},[51,153,46],{"class":61},[51,155,105],{"class":57},[51,157,158,161,164,167,171,174],{"class":53,"line":108},[51,159,160],{"class":111},"  -webkit-text-size-adjust",[51,162,163],{"class":57},": ",[51,165,166],{"class":111},"100",[51,168,170],{"class":169},"szBVR","%",[51,172,173],{"class":57},";   ",[51,175,177],{"class":176},"sJ8bj","\u002F* iOS у ландшафті не роздуває текст *\u002F\n",[51,179,180,183,185,188],{"class":53,"line":118},[51,181,182],{"class":111},"  -webkit-tap-highlight-color",[51,184,163],{"class":57},[51,186,187],{"class":111},"transparent",[51,189,190],{"class":57},";\n",[51,192,193,196,198,201,204],{"class":53,"line":127},[51,194,195],{"class":111},"  touch-action",[51,197,163],{"class":57},[51,199,200],{"class":111},"manipulation",[51,202,203],{"class":57},";       ",[51,205,206],{"class":176},"\u002F* без double-tap-zoom; щипок лишається *\u002F\n",[51,208,210],{"class":53,"line":209},5,[51,211,130],{"class":57},[51,213,215],{"class":53,"line":214},6,[51,216,218],{"emptyLinePlaceholder":217},true,"\n",[51,220,222,225,228,231,234,237],{"class":53,"line":221},7,[51,223,224],{"class":169},"@media",[51,226,227],{"class":57}," (",[51,229,230],{"class":111},"hover",[51,232,233],{"class":57},": none) ",[51,235,236],{"class":169},"and",[51,238,239],{"class":57}," (pointer: coarse) {\n",[51,241,243,246,249,252,254,257,260,263,265,267],{"class":53,"line":242},8,[51,244,245],{"class":61},"  body",[51,247,248],{"class":57}," { ",[51,250,251],{"class":111},"-webkit-user-select",[51,253,163],{"class":57},[51,255,256],{"class":111},"none",[51,258,259],{"class":57},"; ",[51,261,262],{"class":111},"user-select",[51,264,163],{"class":57},[51,266,256],{"class":111},[51,268,269],{"class":57},"; }\n",[51,271,273],{"class":53,"line":272},9,[51,274,275],{"class":176},"  \u002F* iOS не дає друкувати в contenteditable під user-select: none *\u002F\n",[51,277,279,282,285,288,291,294,296,299,302,305],{"class":53,"line":278},10,[51,280,281],{"class":61},"  input",[51,283,284],{"class":57},", ",[51,286,287],{"class":61},"textarea",[51,289,290],{"class":57},", [",[51,292,293],{"class":65},"contenteditable",[51,295,69],{"class":169},[51,297,298],{"class":72},"'true'",[51,300,301],{"class":57},"], ",[51,303,304],{"class":65},".ui-selectable",[51,306,105],{"class":57},[51,308,310,313,315,318,320,322,324,326],{"class":53,"line":309},11,[51,311,312],{"class":111},"    -webkit-user-select",[51,314,163],{"class":57},[51,316,317],{"class":111},"text",[51,319,259],{"class":57},[51,321,262],{"class":111},[51,323,163],{"class":57},[51,325,317],{"class":111},[51,327,190],{"class":57},[51,329,331],{"class":53,"line":330},12,[51,332,333],{"class":57},"  }\n",[51,335,337],{"class":53,"line":336},13,[51,338,130],{"class":57},[10,340,341,344,345,348,349,352,353,356,357,360,361,364],{},[14,342,343],{},"touch-action: manipulation"," на ",[14,346,347],{},"\u003Chtml>"," діє на всю сторінку: частина\n",[14,350,351],{},"touch-action",", що відповідає за зум, областями прокрутки не скидається.\nВідгук на дотик тоді дають власні ",[14,354,355],{},"active:","-стани — а iOS Safari вмикає\n",[14,358,359],{},":active"," лише за наявності обробника ",[14,362,363],{},"touchstart",", тож один раз на\nзастосунок:",[42,366,370],{"className":367,"code":368,"language":369,"meta":47,"style":47},"language-ts shiki shiki-themes github-light github-dark","import { enableTouchActiveStates } from '~\u002Futils\u002Ftouch'\nenableTouchActiveStates()\n","ts",[14,371,372,386],{"__ignoreMap":47},[51,373,374,377,380,383],{"class":53,"line":54},[51,375,376],{"class":169},"import",[51,378,379],{"class":57}," { enableTouchActiveStates } ",[51,381,382],{"class":169},"from",[51,384,385],{"class":72}," '~\u002Futils\u002Ftouch'\n",[51,387,388,391],{"class":53,"line":108},[51,389,390],{"class":65},"enableTouchActiveStates",[51,392,393],{"class":57},"()\n",[138,395,397],{"id":396},"масштаб-на-дотику","Масштаб на дотику",[10,399,400,401,404,405,407,408,411],{},"Корінь 15px — щільність для миші. На дотиковому пристрої застосунок\nприродніше виглядає з коренем 16px: усі ",[14,402,403],{},"rem"," (кегль, відступи, висоти\nшапок і кнопок) ростуть на ~7%, ближче до нативних 16–17pt. Межі ",[14,406,16],{},"\u002F",[14,409,410],{},"xl:","\nвід цього не зсуваються — rem у медіазапиті рахується від шрифту браузера.",[42,413,415],{"className":94,"code":414,"language":96,"meta":47,"style":47},"@media (pointer: coarse) {\n  html { font-size: 16px; }\n}\n",[14,416,417,423,443],{"__ignoreMap":47},[51,418,419,421],{"class":53,"line":54},[51,420,224],{"class":169},[51,422,239],{"class":57},[51,424,425,428,430,433,435,438,441],{"class":53,"line":108},[51,426,427],{"class":61},"  html",[51,429,248],{"class":57},[51,431,432],{"class":111},"font-size",[51,434,163],{"class":57},[51,436,437],{"class":111},"16",[51,439,440],{"class":169},"px",[51,442,269],{"class":57},[51,444,445],{"class":53,"line":118},[51,446,130],{"class":57},[10,448,449],{},"Перевірте тоді власні числа в px, що залежать від кегля (оцінки висот\nрядків у віртуальних списках): їх треба множити на корінь документа.",[138,451,453],{"id":452},"колір-рядка-стану","Колір рядка стану",[10,455,456,457,460,461,464,465,468],{},"Статичні ",[14,458,459],{},"\u003Cmeta name=\"theme-color\" media=\"…\">"," знають лише тему ОС, а тему\nперемикають і вручну. ",[14,462,463],{},"startThemeColor"," тримає рядок стану телефона й панель\nбраузера в кольорі шапки поточної теми застосунку (",[14,466,467],{},"--bg-card"," або свій\nтокен); поверхня на весь екран — відеодзвінок, перегляд — кладе свій колір\nповерх і знімає, коли закривається.",[42,470,472],{"className":367,"code":471,"language":369,"meta":47,"style":47},"import { pushThemeColorSurface, startThemeColor } from '~\u002Futils\u002FthemeColor'\nstartThemeColor()                                   \u002F\u002F один раз на застосунок\nconst restore = pushThemeColorSurface('--stage')    \u002F\u002F дзвінок на весь екран\n",[14,473,474,486,496],{"__ignoreMap":47},[51,475,476,478,481,483],{"class":53,"line":54},[51,477,376],{"class":169},[51,479,480],{"class":57}," { pushThemeColorSurface, startThemeColor } ",[51,482,382],{"class":169},[51,484,485],{"class":72}," '~\u002Futils\u002FthemeColor'\n",[51,487,488,490,493],{"class":53,"line":108},[51,489,463],{"class":65},[51,491,492],{"class":57},"()                                   ",[51,494,495],{"class":176},"\u002F\u002F один раз на застосунок\n",[51,497,498,501,504,507,510,513,516,519],{"class":53,"line":118},[51,499,500],{"class":169},"const",[51,502,503],{"class":111}," restore",[51,505,506],{"class":169}," =",[51,508,509],{"class":65}," pushThemeColorSurface",[51,511,512],{"class":57},"(",[51,514,515],{"class":72},"'--stage'",[51,517,518],{"class":57},")    ",[51,520,521],{"class":176},"\u002F\u002F дзвінок на весь екран\n",[138,523,525],{"id":524},"системне-назад","Системне «назад»",[10,527,528,529,532,533,536],{},"Кнопка чи жест «назад» Android і «назад» браузера на телефоні мають\nспершу закривати верхню шторку, меню чи вікно — так само, як Escape, — і\nлише потім іти з екрана. ",[14,530,531],{},"useSystemBack"," робить це для всіх оверлеїв\nбібліотеки (і для шарів застосунку з ",[14,534,535],{},"layers","): поки шар відкритий, поверх\nзапису історії лежить запис-охоронець з тією самою адресою.",[42,538,540],{"className":367,"code":539,"language":369,"meta":47,"style":47},"\u002F\u002F main.ts — ПЕРШИМ рядком, до імпорту роутера\nimport '~\u002Futils\u002FpopstateFirst'\n\u002F\u002F …\nuseSystemBack({ router, layers: [{ open: () => callFull.value, close: () => minimize() }] })\n",[14,541,542,547,554,559],{"__ignoreMap":47},[51,543,544],{"class":53,"line":54},[51,545,546],{"class":176},"\u002F\u002F main.ts — ПЕРШИМ рядком, до імпорту роутера\n",[51,548,549,551],{"class":53,"line":108},[51,550,376],{"class":169},[51,552,553],{"class":72}," '~\u002Futils\u002FpopstateFirst'\n",[51,555,556],{"class":53,"line":118},[51,557,558],{"class":176},"\u002F\u002F …\n",[51,560,561,563,566,569,572,575,578,581,583,585,588],{"class":53,"line":127},[51,562,531],{"class":65},[51,564,565],{"class":57},"({ router, layers: [{ ",[51,567,568],{"class":65},"open",[51,570,571],{"class":57},": () ",[51,573,574],{"class":169},"=>",[51,576,577],{"class":57}," callFull.value, ",[51,579,580],{"class":65},"close",[51,582,571],{"class":57},[51,584,574],{"class":169},[51,586,587],{"class":65}," minimize",[51,589,590],{"class":57},"() }] })\n",[10,592,593,594,597,598,601,602,605,606,609,610,613,614,617],{},"Слухач ",[14,595,596],{},"popstate"," має стати раніше за роутер: тоді крок назад, що зняв\nохоронця, роутер не бачить узагалі. У Nuxt роутер створює плагін\nфреймворку раніше за будь-який імпорт, тож той самий слухач ставить\nінлайн-скрипт ",[14,599,600],{},"POPSTATE_FIRST_SCRIPT"," в ",[14,603,604],{},"app.head.script",". Програмний крок\nназад (",[14,607,608],{},"router.back()"," з кнопки «Назад») — після ",[14,611,612],{},"await releaseSystemBack()",":\nінакше він зняв би охоронця щойно закритого вікна, а не екран. З мишею\n«назад» браузера лишається навігацією (",[14,615,616],{},"touchOnly",").",[19,619,621],{"id":620},"ціль-дотику-44px-зони-а-не-заливки","Ціль дотику — 44px зони, а не заливки",[10,623,624],{},"Точність дотику вимірюється цільовою зоною (W3C 2.5.8), а не видимою\nвисотою. Тому ієрархія розмірів на телефоні лишається, а дотик тримають\nтри прийоми:",[626,627,628,643,661],"ul",{},[629,630,631,634,635,638,639,642],"li",{},[26,632,633],{},"Фізична висота 45px"," там, де елемент і є рядком: поля вводу (",[14,636,637],{},"h-12","),\nпункти випадайок і меню, картки вибору в ",[14,640,641],{},"UiRadioGroup",", рядки\nналаштувань колонок таблиці.",[629,644,645,648,649,652,653,656,657,660],{},[26,646,647],{},"Невидима зона 45×45"," через ",[14,650,651],{},"pointer-coarse:after:"," навколо дрібних\nелементів: кнопки, хрестики очищення, квадратик ",[14,654,655],{},"UiCheckbox",", кнопки\nтулбара редактора, посилання ",[14,658,659],{},"UiBreadcrumb",", заголовки колонок, що\nсортуються. На пристрої з мишею зони немає.",[629,662,663,666,667,670],{},[26,664,665],{},"Гнучка сітка",", де елементи стоять впритул і зона накрила б сусіда:\nклітинка дня в ",[14,668,669],{},"UiCalendar"," росте до 45px, але не ширша за сьому частину\nекрана — на 320px вона стискається, а сітка лишається в екрані.",[10,672,673,674,677],{},"Для власних пунктів меню в слоті ",[14,675,676],{},"content"," є спільний клас — висота 45px на\nтелефоні, щільно на десктопі. Колір тексту додається окремо:",[42,679,683],{"className":680,"code":681,"language":682,"meta":47,"style":47},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\nimport { menuItemClass } from '~\u002Futils\u002FuiFieldStyles'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton role=\"menuitem\" :class=\"[menuItemClass, 'text-danger']\">Видалити\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n","vue",[14,684,685,705,717,726,730,739,770],{"__ignoreMap":47},[51,686,687,689,692,695,698,700,703],{"class":53,"line":54},[51,688,58],{"class":57},[51,690,691],{"class":61},"script",[51,693,694],{"class":65}," setup",[51,696,697],{"class":65}," lang",[51,699,69],{"class":57},[51,701,702],{"class":72},"\"ts\"",[51,704,84],{"class":57},[51,706,707,709,712,714],{"class":53,"line":108},[51,708,376],{"class":169},[51,710,711],{"class":57}," { menuItemClass } ",[51,713,382],{"class":169},[51,715,716],{"class":72}," '~\u002Futils\u002FuiFieldStyles'\n",[51,718,719,722,724],{"class":53,"line":118},[51,720,721],{"class":57},"\u003C\u002F",[51,723,691],{"class":61},[51,725,84],{"class":57},[51,727,728],{"class":53,"line":127},[51,729,218],{"emptyLinePlaceholder":217},[51,731,732,734,737],{"class":53,"line":209},[51,733,58],{"class":57},[51,735,736],{"class":61},"template",[51,738,84],{"class":57},[51,740,741,744,747,750,752,755,758,760,763,766,768],{"class":53,"line":214},[51,742,743],{"class":57},"  \u003C",[51,745,746],{"class":61},"button",[51,748,749],{"class":65}," role",[51,751,69],{"class":57},[51,753,754],{"class":72},"\"menuitem\"",[51,756,757],{"class":65}," :class",[51,759,69],{"class":57},[51,761,762],{"class":72},"\"[menuItemClass, 'text-danger']\"",[51,764,765],{"class":57},">Видалити\u003C\u002F",[51,767,746],{"class":61},[51,769,84],{"class":57},[51,771,772,774,776],{"class":53,"line":221},[51,773,721],{"class":57},[51,775,736],{"class":61},[51,777,84],{"class":57},[19,779,781],{"id":780},"текст-полів-16px","Текст полів — 16px",[10,783,784,785,787,788,791,792,795,796,601,799,802,803,806,807,810,811,813,814,817],{},"iOS Safari при фокусі на поле з ",[14,786,432],{}," менше 16px автоматично зумує\nсторінку. Корінь документа — 15px, тож ",[14,789,790],{},"text-base"," (1rem) дає 15px і\nтеж зумує (з коренем 16px на дотику — див. вище — вже ні, але піксельне\nзначення лишається надійнішим). Тому текст полів на мобільному — ",[14,793,794],{},"text-[16px]"," у пікселях\n(",[14,797,798],{},"fieldTextSizes",[14,800,801],{},"uiFieldStyles.ts","). Те саме стосується пошуку\nвсередині випадайок, прихованого поля ",[14,804,805],{},"UiInputOtp"," і самого редактора\n",[14,808,809],{},"UiRichTextEditor"," — ",[14,812,293],{}," зумує так само, як ",[14,815,816],{},"\u003Cinput>",".",[19,819,821],{"id":820},"клавіатура-і-видима-область","Клавіатура і видима область",[10,823,824,825,828,829,832,833,836,837,840],{},"Екранна клавіатура на iOS і на Android (з Chrome 108) ",[26,826,827],{},"не"," зменшує\n",[14,830,831],{},"innerHeight",": випадайка під полем, яка за цим числом «влазить», фізично\nлежить під клавіатурою. Усі плаваючі панелі бібліотеки рахують позицію від\n",[14,834,835],{},"visualViewport"," і переміщуються, коли клавіатура виїжджає чи ховається:\nпанель перевертається догори або отримує ",[14,838,839],{},"max-height"," і прокрутку.",[10,842,843,844,91],{},"Для власного оверлея — ті самі функції з ",[14,845,846],{},"utils\u002FoverlayPosition.ts",[42,848,850],{"className":367,"code":849,"language":369,"meta":47,"style":47},"const viewport = getVisibleViewport()          \u002F\u002F видима область, а не innerHeight\nconst point = placeAnchoredPanel(anchor.getBoundingClientRect(), panel, viewport)\nconst stop = listenViewportChanges(updatePosition) \u002F\u002F scroll, resize, клавіатура\n",[14,851,852,870,891],{"__ignoreMap":47},[51,853,854,856,859,861,864,867],{"class":53,"line":54},[51,855,500],{"class":169},[51,857,858],{"class":111}," viewport",[51,860,506],{"class":169},[51,862,863],{"class":65}," getVisibleViewport",[51,865,866],{"class":57},"()          ",[51,868,869],{"class":176},"\u002F\u002F видима область, а не innerHeight\n",[51,871,872,874,877,879,882,885,888],{"class":53,"line":108},[51,873,500],{"class":169},[51,875,876],{"class":111}," point",[51,878,506],{"class":169},[51,880,881],{"class":65}," placeAnchoredPanel",[51,883,884],{"class":57},"(anchor.",[51,886,887],{"class":65},"getBoundingClientRect",[51,889,890],{"class":57},"(), panel, viewport)\n",[51,892,893,895,898,900,903,906],{"class":53,"line":118},[51,894,500],{"class":169},[51,896,897],{"class":111}," stop",[51,899,506],{"class":169},[51,901,902],{"class":65}," listenViewportChanges",[51,904,905],{"class":57},"(updatePosition) ",[51,907,908],{"class":176},"\u002F\u002F scroll, resize, клавіатура\n",[10,910,911,912,915],{},"Прокручувані панелі мають ",[14,913,914],{},"overscroll-contain",": докрутивши список до кінця,\nпалець не тягне за собою сторінку під оверлеєм.",[19,917,919],{"id":918},"жести","Жести",[921,922,923,936],"table",{},[924,925,926],"thead",{},[927,928,929,933],"tr",{},[930,931,932],"th",{},"Компонент",[930,934,935],{},"На дотику",[937,938,939,950,960,978,988,998,1016,1026,1036,1046],"tbody",{},[927,940,941,947],{},[942,943,944],"td",{},[14,945,946],{},"UiTooltip",[942,948,949],{},"Довге натискання (0,5 с) показує підказку, відпускання ховає її через 1,5 с. Клік після довгого натискання гаситься — людина хотіла прочитати, а не натиснути.",[927,951,952,957],{},[942,953,954],{},[14,955,956],{},"UiHoverCard",[942,958,959],{},"Не відкривається. Наведення немає, а картка, що відкривалася від тапу по посиланню, блимала перед переходом.",[927,961,962,967],{},[942,963,964],{},[14,965,966],{},"UiContextMenu",[942,968,969,970,973,974,977],{},"Довге натискання — на Android через ",[14,971,972],{},"contextmenu",", на iOS власним таймером. З ",[14,975,976],{},"haptic"," — короткий відгук вібрацією (Android).",[927,979,980,985],{},[942,981,982],{},[14,983,984],{},"UiCarousel",[942,986,987],{},"Свайп із фліком і блокуванням осі; стрілок на дотику немає.",[927,989,990,995],{},[942,991,992],{},[14,993,994],{},"UiDrawer",[942,996,997],{},"Ручка свайпу закриває панель.",[927,999,1000,1005],{},[942,1001,1002],{},[14,1003,1004],{},"UiModal",[942,1006,1007,1008,1011,1012,1015],{},"З ",[14,1009,1010],{},"swipe-to-close"," шторка (до ",[14,1013,1014],{},"sm",") має ручку й закривається свайпом униз за ручку чи шапку.",[927,1017,1018,1023],{},[942,1019,1020],{},[14,1021,1022],{},"UiSwipeActions",[942,1024,1025],{},"Свайп рядка ліворуч відкриває дії під ним; дотик повз, прокрутка чи свайп праворуч закривають.",[927,1027,1028,1033],{},[942,1029,1030],{},[14,1031,1032],{},"UiSwipeStack",[942,1034,1035],{},"Свайп праворуч з будь-якого місця верхнього екрана тягне його за пальцем і повертає назад.",[927,1037,1038,1043],{},[942,1039,1040],{},[14,1041,1042],{},"UiToaster",[942,1044,1045],{},"Картку можна змахнути вбік.",[927,1047,1048,1050],{},[942,1049,525],{},[942,1051,1007,1052,1054],{},[14,1053,531],{}," закриває верхній оверлей, а не екран під ним.",[138,1056,1058],{"id":1057},"власні-жести","Власні жести",[10,1060,1061,1062,1065,1066,1068,1069,1071],{},"Ядро горизонтальних свайпів — ",[14,1063,1064],{},"utils\u002Fswipe.ts",", спільне для ",[14,1067,1022],{},"\nі ",[14,1070,1032],{},": блокування осі (вертикаль лишається прокрутці), поріг і\nкидок, опір після межі, гасіння кліку, що прийшов слідом за свайпом.",[42,1073,1075],{"className":367,"code":1074,"language":369,"meta":47,"style":47},"import { bindSwipe, rubberBand } from '~\u002Futils\u002Fswipe'\n\nconst stop = bindSwipe(listEl, {\n  direction: 'left',\n  onMove: (dx) => (row.style.transform = `translateX(${rubberBand(dx, 84)}px)`),\n  onEnd: ({ dx, cancelled }) => { if (!cancelled && dx \u003C -56) reply() },\n})\n",[14,1076,1077,1089,1093,1107,1118,1164,1219],{"__ignoreMap":47},[51,1078,1079,1081,1084,1086],{"class":53,"line":54},[51,1080,376],{"class":169},[51,1082,1083],{"class":57}," { bindSwipe, rubberBand } ",[51,1085,382],{"class":169},[51,1087,1088],{"class":72}," '~\u002Futils\u002Fswipe'\n",[51,1090,1091],{"class":53,"line":108},[51,1092,218],{"emptyLinePlaceholder":217},[51,1094,1095,1097,1099,1101,1104],{"class":53,"line":118},[51,1096,500],{"class":169},[51,1098,897],{"class":111},[51,1100,506],{"class":169},[51,1102,1103],{"class":65}," bindSwipe",[51,1105,1106],{"class":57},"(listEl, {\n",[51,1108,1109,1112,1115],{"class":53,"line":127},[51,1110,1111],{"class":57},"  direction: ",[51,1113,1114],{"class":72},"'left'",[51,1116,1117],{"class":57},",\n",[51,1119,1120,1123,1126,1130,1133,1135,1138,1140,1143,1146,1148,1150,1152,1155,1158,1161],{"class":53,"line":209},[51,1121,1122],{"class":65},"  onMove",[51,1124,1125],{"class":57},": (",[51,1127,1129],{"class":1128},"s4XuR","dx",[51,1131,1132],{"class":57},") ",[51,1134,574],{"class":169},[51,1136,1137],{"class":57}," (row.style.transform ",[51,1139,69],{"class":169},[51,1141,1142],{"class":72}," `translateX(${",[51,1144,1145],{"class":65},"rubberBand",[51,1147,512],{"class":72},[51,1149,1129],{"class":57},[51,1151,284],{"class":72},[51,1153,1154],{"class":111},"84",[51,1156,1157],{"class":72},")",[51,1159,1160],{"class":72},"}px)`",[51,1162,1163],{"class":57},"),\n",[51,1165,1166,1169,1172,1174,1176,1179,1182,1184,1186,1189,1191,1194,1197,1200,1203,1205,1208,1211,1213,1216],{"class":53,"line":214},[51,1167,1168],{"class":65},"  onEnd",[51,1170,1171],{"class":57},": ({ ",[51,1173,1129],{"class":1128},[51,1175,284],{"class":57},[51,1177,1178],{"class":1128},"cancelled",[51,1180,1181],{"class":57}," }) ",[51,1183,574],{"class":169},[51,1185,248],{"class":57},[51,1187,1188],{"class":169},"if",[51,1190,227],{"class":57},[51,1192,1193],{"class":169},"!",[51,1195,1196],{"class":57},"cancelled ",[51,1198,1199],{"class":169},"&&",[51,1201,1202],{"class":57}," dx ",[51,1204,58],{"class":169},[51,1206,1207],{"class":169}," -",[51,1209,1210],{"class":111},"56",[51,1212,1132],{"class":57},[51,1214,1215],{"class":65},"reply",[51,1217,1218],{"class":57},"() },\n",[51,1220,1221],{"class":53,"line":221},[51,1222,1223],{"class":57},"})\n",[10,1225,1226,1227,1230,1231,1234,1235,1238,1239,1242,1243,1246],{},"Область жесту й кожна область прокрутки в ній мають ",[14,1228,1229],{},"touch-action: pan-y pinch-zoom"," (класи ",[14,1232,1233],{},"touch-pan-y touch-pinch-zoom","): інакше браузер забирає\nгоризонтальний рух собі й шле ",[14,1236,1237],{},"pointercancel",". Жест не починається з полів,\nповзунків, полотна й будь-чого з ",[14,1240,1241],{},"touch-action: none"," (навіть інлайн-\nстилем); решту виключає атрибут ",[14,1244,1245],{},"data-no-swipe",". Миша свайпом не\nкористується.",[19,1248,1250],{"id":1249},"розкладка-на-вузьких-екранах","Розкладка на вузьких екранах",[626,1252,1253,1261,1267,1276,1289,1298],{},[629,1254,1255,1257,1258,1260],{},[14,1256,1004],{}," нижче ",[14,1259,1014],{}," — нижній sheet, а не вікно посередині.",[629,1262,1263,1266],{},[14,1264,1265],{},"UiDateRangePicker"," на телефоні показує один місяць, а пресети — смугою\nпігулок над сіткою, що прокручується вбік.",[629,1268,1269,1272,1273,1275],{},[14,1270,1271],{},"UiStepper"," у горизонтальному ряду нижче ",[14,1274,1014],{}," показує підпис лише\nактивного кроку; решта — кружечки й лінії, підписи лишаються для\nскрінрідера.",[629,1277,1278,1257,1281,1284,1285,1288],{},[14,1279,1280],{},"UiTable",[14,1282,1283],{},"md"," — картки. Шапки там немає, тож сортування живе в\nокремому рядку над картками: нативний ",[14,1286,1287],{},"\u003Cselect>"," і кнопка напрямку.",[629,1290,1291,1294,1295,617],{},[14,1292,1293],{},"UiButton"," не переносить підпис: висота кнопки фіксована, і другий рядок\nвиходив би за заливку. Ряд кнопок, що не влазить, має переноситися сам\n(",[14,1296,1297],{},"flex-wrap",[629,1299,1300,1302,1303,1306],{},[14,1301,984],{}," і ",[14,1304,1305],{},"UiVirtualList"," займають усю ширину батька.",[19,1308,1310],{"id":1309},"як-перевіряти","Як перевіряти",[10,1312,1313,1314,1317,1318,1321,1322,1324],{},"Режим пристрою в DevTools з увімкненим дотиком: без нього ",[14,1315,1316],{},"pointer: coarse","\nне спрацьовує, і невидимих зон просто не видно. Свайпи в автотестах —\nсправжніми дотиками (у Chromium — CDP ",[14,1319,1320],{},"Input.dispatchTouchEvent","): на відміну\nвід синтетичних PointerEvent вони поважають ",[14,1323,351],{},", тож видно, коли\nбраузер забирає жест собі. Перевіряти на 320px і\n375px — у 320px першими виходять за край ряди кнопок і сітки. На справжньому\niPhone окремо варто перевірити фокус у полі (зум) і відкриту клавіатуру\nпід випадайкою.",[1326,1327,1328],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":47,"searchDepth":108,"depth":108,"links":1330},[1331,1337,1338,1339,1340,1343,1344],{"id":21,"depth":108,"text":22,"children":1332},[1333,1334,1335,1336],{"id":140,"depth":118,"text":141},{"id":396,"depth":118,"text":397},{"id":452,"depth":118,"text":453},{"id":524,"depth":118,"text":525},{"id":620,"depth":108,"text":621},{"id":780,"depth":108,"text":781},{"id":820,"depth":108,"text":821},{"id":918,"depth":108,"text":919,"children":1341},[1342],{"id":1057,"depth":118,"text":1058},{"id":1249,"depth":108,"text":1250},{"id":1309,"depth":108,"text":1310},null,[],"Цілі дотику, 16px у полях, видима область із клавіатурою, безпечні зони, жести, системне «назад» і нативне відчуття — що робить бібліотека і що має зробити проєкт.",{},{},"\u002Fdocs\u002Ffoundations\u002Fmobile",{"title":5,"description":1347},"stable","docs\u002Ffoundations\u002Fmobile","QW3rkCnhQWo_xo-8rWkCMc5LPgLKWEkbOonrB0Q8DKA",1792004323355]