Если у вас один продукт живёт на двух фронтах — обычный веб для мобилок и Telegram Mini App — то рано или поздно они разъедутся, даже когда форм-фактор у них один и тот же. И переиспользование компонентов вас от этого не спасёт, потому что переиспользуется обычно сам контрол, а вот рамка вокруг него, которая прибивает его к низу экрана и расставляет отступы, пишется на каждом экране руками заново. Именно это я и поймал в Картаре 14 июня: красная точка в навигации работает в Mini App и молчит на вебе, хедеры разные, инпуты разные, чаты ведут себя по-своему, а корень одного из багов оказался настолько показательным, что я решил про это написать. Если коротко: лечить надо не симптом, а общую обёртку.
Контекст для тех, кто не в теме Картары. Это Telegram Mini App с AI-персонажем, и у него два клиента. Один — обычный веб-мобильный, который человек открывает в браузере, второй — та же штука внутри Telegram. По задумке это одно и то же приложение, один форм-фактор, одна логика. Но разработка-то идёт волнами, агентами, я оркестрирую Claude Code и гоню фичи туда, где они сейчас горят, а не сразу симметрично на оба фронта. И вот за несколько месяцев такого режима два фронта тихо расползлись — про то, почему Mini App и веб вечно расходятся и сколько стоит держать их в паритете, у меня уже был отдельный разбор.
Что именно разъехалось
Я сел и прогнал аудит расхождений между вебом и Mini App, и первым делом раскидал их на две кучки: законные и дрейф. Законные — это когда платформа реально требует другого поведения. Телеграм диктует свои правила по верхней зоне, по safe-area, по тому, как ведёт себя клавиатура внутри вебвью, и тут разный код это нормально, так и должно быть. А дрейф это когда расхождение случайное, никто его не закладывал, оно просто наросло, потому что один экран писали в одну сессию, другой в другую, и руки сделали чуть иначе.
И вот когда раскидываешь по этим двум кучкам, сразу видно масштаб бедствия. Разные хедеры — дрейф. Разные инпуты — дрейф. Чаты чуть по-разному себя ведут — тоже дрейф. Красная точка в навигации, которая в Mini App горит, а на вебе нет, — опять же дрейф, никто не хотел, чтобы она работала только на одном фронте, просто на втором забыли. То есть это не два разных продукта разъехались, это один продукт сам себе наплодил мелких отличий, и каждое по отдельности выглядит как ерунда, а вместе они дают ощущение, что веб и Mini App собирали два разных человека в разное время. Что, по сути, и правда, только этот человек я, а исполнитель меняется от сессии к сессии.
Почему переиспользование не спасло
А вот тут самое интересное, и тут я хочу копнуть поглубже, потому что у любого, кто слышал слово «компонент», в голове сразу всплывает: ну так переиспользуй, в чём проблема. А проблема в том, что переиспользование как раз было. Вот как я сам это сформулировал, когда докопался до корня: мы переиспользуем поле ввода чата, но это только сам контрол, а прибить его к низу плюс отступы по бокам делает обёртка, которую каждый экран пишет руками заново. Это и есть дыра: контрол общий, а позиционирующая рамка — копипаста на четырёх экранах. Я как раз отдельно разбирал, где код реально переиспользуется, а где только кажется — и это ровно тот же подвох.
Объясню простыми словами. Есть компонент поля ввода — поле плюс кнопки рядом, та самая нижняя строка чата. Он один на всё приложение, его правишь в одном месте, и он меняется везде, всё честно. Но сам по себе он висит в воздухе. Чтобы он встал куда надо, прижался к низу экрана, получил отступы по краям, нормально повёл себя при появлении клавиатуры, его надо во что-то завернуть. И вот эту обёртку, эти несчастные выравнивания, центрирование, зазор между элементами, каждый экран чата писал у себя руками. Четыре экрана — четыре копии одной и той же раскладки.
И пока копии одинаковые, никто ничего не замечает, всё выглядит как переиспользование, хотя на самом деле это копипаста с лишним шагом. А ломается всё в тот момент, когда на одном из четырёх экранов кто-то — я через агента — поправил отступ или добавил условие, а на остальных трёх не тронул. Контрол-то общий и выглядит одинаково, а ведёт себя по-разному, потому что вокруг него четыре разные рамки. Вот вам и дрейф: ты уверен, что переиспользуешь, а переиспользуешь только начинку, а коробка вокруг неё каждый раз новая.
Это, кстати, общая ловушка вайбкодинга, и не только моего. Когда код пишет нейросеть под твоим управлением, а ты раздаёшь задачи по экранам, агенту проще всего решить задачу локально — вот этот экран, вот его разметка, прибил инпут к низу, поехали дальше. Он не видит, что точно такую же рамку он уже три раза собрал на соседних экранах, потому что ты его про это не спросил, а сам по себе он в твою голову с архитектурной картой не лезет. И получается рабочий код, который при этом тихо размножает дублирование. А что, кожаные разработчики тут не грешат ровно тем же? не смешите меня, грешат, просто медленнее.
Решение: обёртки экранов, а не заплатки
Когда я это понял, напрашивался очевидный быстрый путь: найти расхождение, выровнять отступ на проблемном экране и закрыть тикет. Но это лечение симптома, потому что завтра я трону пятый экран и получу пятую копию, послезавтра шестую. Поэтому вывод другой — нужны общие обёртки, я их называю shells, отдельно для экранов чатов, для раскладов и для фри-чатов. То есть переиспользуемым становится не только само поле ввода, но и вся позиционирующая рамка вокруг него, и тогда экрану остаётся сказать «вот сюда мой инпут», а как он встанет, прижмётся и расставит отступы, решает обёртка в одном месте на всех.
И вот тут, кстати, родился вопрос, с которого по сути всё и сдвинулось: вот нам обёртки этих экранов тоже по идее создать надо бы, да? Да. Именно их. Потому что это убивает причину, а не симптом — выравниваешь рамку один раз, и красная точка, хедеры, поведение клавиатуры перестают разъезжаться сами собой, им просто негде разойтись, рамка-то одна.
Почему я не сделал это сразу
А теперь честно про то, чего не было. Я этот рефактор не сделал в тот же день, я положил его в очередь. Звучит как «потом поправлю», против которого я обычно сам же и выступаю, но тут причина не в лени. Переезд четырёх экранов на общие обёртки — операция рискованная, она трогает то, как всё прибивается и отрисовывается, и любая мелкая ошибка в общей рамке мгновенно ломает сразу все экраны, а не один. Тесты тут не страховка: я уже не раз ловил ситуацию, когда тесты зелёные, а экран пустой или поехал, и про это у меня даже отдельная история была — четыре косяка Claude Code за четыре дня, где «готово» от агента и реальный экран расходились.
Поэтому такой рефактор нельзя просто прогнать агентом и закоммитить по зелёным тестам, его надо проверять глазами на обоих фронтах живьём: открыть веб, открыть Mini App в телеграме, потыкать каждый экран чата, посмотреть, как ведёт себя клавиатура, не уехал ли инпут. А это значит, что я должен сесть и руками прокликать оба клиента, а не просто дать команду из машины. Отсюда и очередь: не «забил», а жду окна, когда смогу нормально отсмотреть результат.
Что я из этого вынес
Главный вывод простой и неприятный: переиспользование компонента не равно отсутствию дублирования. Можно гордо тащить одно поле ввода по всему приложению и при этом четыре раза копировать рамку вокруг него, и именно рамка, а не контрол, расползётся первой, потому что её-то никто компонентом не считает. Так что когда смотрите на свой проект и думаете «ну у меня же всё на переиспользуемых компонентах», задайте себе вопрос поехиднее: а где живёт позиционирование, отступы, прижатие к низу — внутри общего компонента или в разметке каждого экрана? Если в разметке, то поздравляю, у вас зреет дрейф, просто вы его пока не видите.
И второй вывод, уже про два фронта вообще. Если у продукта два клиента на одном форм-факторе, дрейф между ними при волновой разработке неизбежен, и единственная защита тут не дисциплина в духе «не забывать делать симметрично», на дисциплину тут надеяться глупо, а архитектура, где у обоих фронтов общая рамка и расходиться им просто негде. Про то, как у Картары вообще устроены эти два фронта и как один баг с сессией показал разницу между ними, я уже писал в истории про приватность устройства — там та же болячка с другого боку. А этот рефактор обёрток лежит у меня в очереди, и когда я его прокатаю на обоих фронтах живьём, расскажу, что отвалилось. Потому что что-нибудь да отвалится, как ни крути.