26 KiB
NON-NEGOTIABLE ARCHITECTURE RULES
- Read this file before editing.
- App/Router is the only page mount/unmount owner.
- Never restore self render + replaceWith.
- Never restore components.css.
- Never restore global button:not(...) visual reset.
- Do not add visual !important to solve cascade problems.
- Do not create fixes.css / overrides.css / compatibility.css.
- Reuse TopBar, Dropdown, Avatar and other shared components.
- Preserve accepted visual behavior unless the task explicitly requests redesign.
AGENTS.md
Назначение
Этот файл — обязательный архитектурный контракт проекта shine-UI.
Любой агент, разработчик или автоматический инструмент перед изменением UI обязан сначала прочитать этот файл.
Главная задача правил — не только сделать новую функцию рабочей, но и не вернуть архитектурные проблемы, которые уже были устранены предыдущими refactor stages.
Если быстрое решение конфликтует с этими правилами — быстрое решение запрещено.
1. Общая архитектура
Проект — vanilla JavaScript frontend.
Не использовать и не добавлять без отдельного архитектурного решения:
React
Vue
Svelte
Solid
Preact
Redux
глобальный state framework
virtual DOM framework
новый router
Текущая архитектурная цепочка:
Foundation
↓
App Shell
↓
Shared Components
↓
Semantic UI roles
↓
Features / Pages
Направление зависимостей должно сохраняться.
Feature может использовать shared component.
Shared component не должен знать детали конкретного feature.
2. Зафиксированный baseline
Следующие этапы считаются завершёнными и не должны откатываться.
Stage 1 — App Shell
Shell централизован:
AppShell
├── TopbarSlot
├── ScreenContent
├── ComposerSlot
├── BottomToolbarSlot
├── Global Top Fade
└── Global Bottom Fade
Shell управляется через chrome.
Stage 2 — Shared TopBar / Dropdown
Используются единые:
js/components/topbar.js
styles/components/topbar.css
js/components/dropdown-menu.js
styles/components/dropdown-menu.css
Не создавать альтернативные Header/Menu реализации внутри страниц.
Stage 3 — CSS ownership
Старый:
styles/components.css
удалён.
Его нельзя создавать снова.
CSS разделён по владельцам:
styles/components/*
styles/features/*
Stage 4 — CSS / Button architecture
Старый глобальный button reset удалён.
Запрещено возвращать архитектуру:
button:not(...):not(...) {
...
}
Количество !important было уменьшено примерно:
356 → 8
Не возвращать specificity wars и late global override layer.
Button styling является opt-in / semantic.
Stage 5 — Page lifecycle
Channel и Thread больше не создают новый instance сами через:
const next = render(...);
current.cleanup?.();
current.replaceWith(next);
Текущая модель:
App / Router
↓
mount page instance
↓
stable page root
↓
page.refresh()
↓
local content update
App / Router
↓
page.cleanup()
↓
unmount
Эту ownership-модель нельзя откатывать.
3. Главное правило ownership
Для любого кода сначала определить:
КТО является владельцем?
У каждого UI-аспекта должен быть один основной owner.
Примеры:
Page lifetime → App / Router
TopBar → TopBar component + chrome
Dropdown lifecycle → Dropdown component
Generic Avatar → Avatar component
Toolbar → Toolbar component
Tabs → Tabs component
Button generic role → semantic button layer
Channel layout → Channel feature
Thread layout → Thread feature
Запрещено исправлять проблему созданием второго владельца.
4. App Shell
Страницы не должны напрямую создавать альтернативный global shell.
Для TopBar использовать:
chrome.setTopbar(...)
Для Composer использовать существующий chrome API.
Для shell mode/fades использовать существующий centralized shell contract.
Не создавать внутри ScreenContent собственные:
global header
global fixed toolbar
альтернативный composer slot
копию top fade
копию bottom fade
если это уже принадлежит AppShell.
5. Page lifecycle
App / Router владеет
mount page root
unmount page root
route change
заменой текущей страницы
shell slot lifecycle
Page владеет
local state
local content
feature listeners
feature timers
feature observers
local refresh
6. Запрещённый self-rerender
Внутри page нельзя делать:
const next = render(...);
screen.cleanup?.();
screen.replaceWith(next);
или эквиваленты:
self render + replaceWith
self render + outerHTML
self cleanup + self recreation
recursive page render
для обновления текущего route.
Если нужно обновить данные текущей страницы:
screen.refresh()
или внутренний:
refresh()
renderBody()
syncView()
должен обновить содержимое существующего instance.
Page root при local refresh должен оставаться тем же DOM node.
7. Stable root
Нормальное поведение:
const rootBefore = screen;
await screen.refresh();
rootBefore === screen;
должно оставаться истинным.
При переходе на другой route App создаёт другой root.
Нельзя делать так, чтобы App хранил reference на detached старый root.
8. Cleanup
Каждая page, создающая lifetime-resources, должна иметь корректный:
screen.cleanup
Cleanup должен быть безопасным при повторном вызове.
Предпочтительный pattern:
let disposed = false;
screen.cleanup = () => {
if (disposed) return;
disposed = true;
// cleanup
};
9. Refresh НЕ является cleanup
Нельзя при каждом local refresh выполнять full page cleanup.
Refresh может очищать только refresh-owned content/resources.
Например допустимо:
старый content
старый read tracker
старые refresh timers
Но нельзя уничтожать весь page instance и создавать новый.
10. Async safety
Любая async операция, результат которой обновляет UI, должна учитывать lifetime страницы.
Использовать существующий pattern:
if (disposed) return;
и при нескольких последовательных refresh:
const seq = ++refreshSeq;
const result = await load();
if (disposed || seq !== refreshSeq) return;
Это предотвращает:
request A
↓
refresh B
↓
B становится актуальным
↓
A завершается позже
↓
A портит новый UI
11. Не мутировать UI после dispose
После:
screen.cleanup()
старые async callbacks не должны:
showToast
перерисовывать screen
открывать modal
менять TopBar
менять Composer
добавлять DOM
если действие относится к уже закрытой странице.
Перед UI commit проверять active state.
12. Timers
Lifetime-sensitive timers должны:
- либо быть безопасными через
isConnected/disposed; - либо сохраняться и очищаться.
Например:
const timer = window.setTimeout(...);
если timer принадлежит refresh/page lifecycle, должен быть очищаемым.
Не оставлять timeout, который после navigation мутирует detached DOM.
13. Listeners
Различать:
Root-owned listener
screen.addEventListener(...)
Если root уничтожается вместе со listener, отдельный remove может быть не нужен.
Global listener
window.addEventListener(...)
document.addEventListener(...)
Он обязан иметь понятный cleanup:
window.removeEventListener(...)
Не создавать anonymous global listener, который невозможно снять.
14. Modal ownership
Если page открывает modal, нужно понимать, кому он принадлежит.
При page cleanup нельзя оставлять modal, callback которого замыкает disposed page state.
Для page-owned modal при unmount:
close / destroy / clear
его UI и callbacks.
Не удалять чужой modal другой feature.
15. Dropdown ownership
Не реализовывать заново:
outside click
Escape
portal
backdrop
pressed state
resize/scroll cleanup
Использовать общий Dropdown component.
Feature передаёт содержимое/actions.
Dropdown component управляет своим lifecycle.
16. TopBar ownership
Не создавать новый <header> непосредственно в feature.
Использовать общий TopBar.
Feature может передавать:
title
left action
right action
center content
callbacks
Но generic geometry/surface TopBar принадлежит shared component.
17. CSS hierarchy
CSS должен следовать:
main / foundation
↓
layout
↓
app-shell
↓
semantic/shared components
↓
features
Нельзя решать cascade problem перестановкой «fix file» в самый конец.
18. Запрещённые CSS-файлы
Не создавать:
components.css
legacy.css
compatibility.css
fixes.css
final.css
final-fixes.css
overrides.css
patches.css
как место, куда складываются новые исключения.
Если правило непонятно куда положить — сначала определить owner.
19. Shared CSS ownership
Generic component surface должна жить в:
styles/components/*
Примеры:
avatar.css
topbar.css
dropdown-menu.css
toolbar.css
tabs.css
modal.css
emoji-picker.css
call-ui.css
scroll-to-bottom.css
attachments.css
Feature CSS не должен возвращать альтернативную реализацию shared component.
20. Feature CSS ownership
В:
styles/features/*
допустимы:
feature layout
feature spacing
feature-specific state
feature-specific intentional variant
Не допустимы случайные переопределения generic shared component surface.
Например feature может задавать Avatar:
size
placement
margin
но не должен случайно заново определять:
generic background
generic frame
generic glass overlay
generic fallback
21. Не добавлять исторические override chains
Запрещён pattern:
.component { ... }
/* fix */
.screen .component { ... }
/* final */
.screen.feature .component { ... }
/* really final */
.screen.feature .component.state {
... !important;
}
Перед добавлением нового rule проверить существующие definitions этого selector.
Если нужен новый final result — изменить правильный owner/base/state.
22. !important
Новые !important по умолчанию запрещены.
Перед добавлением обязательно доказать, почему проблему нельзя решить:
ownership
specificity
source order
state class
semantic role
Текущий низкий baseline нельзя ухудшать.
Особенно запрещены:
background: ... !important;
border: ... !important;
color: ... !important;
box-shadow: ... !important;
для победы в визуальном cascade.
23. Button architecture
Старый глобальный reset возвращать запрещено.
Нельзя:
button:not(...) {
background: transparent;
...
}
Нельзя:
:root button {
...
}
использовать как global visual policy.
24. Перед созданием кнопки определить её роль
Каждый новый <button> должен попасть в одну из категорий:
1. semantic generic button
2. shared component button
3. intentional feature variant
25. Generic semantic button
Если кнопка должна иметь общий neutral Stage-4 surface, использовать существующую semantic role, например:
ui-button
primary-btn
secondary-btn
destructive-btn
ghost-btn
icon-btn
text-btn
shine-btn
Не писать отдельный visual reset для одного feature.
26. Component-owned button
Если button принадлежит:
TopBar
Toolbar
Tabs
Dropdown
EmojiPicker
Call
ScrollToBottom
его surface должен принадлежать CSS этого component.
Не добавлять ui-button автоматически, если component уже имеет собственный intentional contract.
27. Composite buttons
Особенно внимательно с:
<button class="card row ...">
или:
button + card
button + row
button + chip
button + pill
Generic structural class может содержать собственный background/border.
Перед использованием проверить конечный cascade.
Если button должен оставаться semantic neutral — добавить правильную semantic button role.
28. Button states
При изменении button проверять не только normal state.
Минимум:
normal
hover
active
focus-visible
disabled
Где применимо:
is-active
is-selected
is-liked
aria-expanded
data-open
29. Button pseudo-elements
При изменении button также проверить:
::before
::after
Особенно если используются:
glow
shine
press
overlay
decorative line
Не возвращать мёртвые historical pseudo-layers.
30. currentColor
Для icon buttons учитывать:
stroke: currentColor;
fill: currentColor;
Изменение color может визуально изменить SVG даже при неизменном background.
31. Новый shared component
Создавать shared component имеет смысл, если:
- один и тот же UI pattern реально используется в нескольких features;
- у него есть собственный lifecycle или generic visual contract;
- feature-specific код начинает дублироваться.
Не делать shared component из элемента, существующего только на одной странице.
32. Новый feature CSS
При добавлении feature rule сначала ответить:
Это generic component surface?
или
это только feature layout/state?
Если generic — правило, скорее всего, должно находиться в styles/components/*.
Если feature-specific — в соответствующем styles/features/*.
33. Не исправлять один экран ломая owner
Если визуальная проблема возникает только в Channel, нельзя автоматически писать:
.channel-screen .shared-component {
...
}
Сначала проверить:
- shared component сломан глобально?
- Channel использует неправильный variant?
- feature содержит historical declaration?
- неправильный DOM class?
Исправлять первопричину.
34. App/router не переписывать локальной задачей
Feature change не должна создавать:
новый router
parallel navigation
собственный page registry
собственный mount manager
Навигация выполняется через существующий:
navigate(...)
35. Refresh ≠ navigation
Если пользователь остаётся на той же странице:
like
reply
edit
delete
subscribe
metadata update
это обычно local refresh/update.
Не симулировать refresh через:
navigate(currentRoute)
если нет специальной архитектурной причины.
36. Route contract
Не менять без отдельной задачи:
route names
route params
deep links
history semantics
Lifecycle refactor не должен менять URL contract.
37. Backend/API
UI-агент не должен без отдельной задачи менять:
API contract
backend
DAO
blockchain protocol
response format
storage schema
Если для UI не хватает данных — зафиксировать проблему, а не придумывать новый backend contract.
38. Перед редактированием
Перед любой нетривиальной правкой агент обязан:
- найти DOM producer;
- определить owner;
- найти все связанные selectors/functions;
- проверить shared component;
- проверить существующие states;
- проверить cleanup/lifecycle;
- определить baseline текущего поведения.
Не начинать с добавления override.
39. После редактирования
Минимальный обязательный validation:
node --check для production JS
relative import audit
CSS parser если CSS менялся
CSS manifest если CSS/imports менялись
!important count если CSS менялся
duplicate-selector audit для затронутого owner
40. Для lifecycle изменений
Дополнительно проверить:
self render + replaceWith отсутствует
root identity сохраняется при refresh
cleanup idempotent
async callback после dispose ничего не мутирует
refresh race защищён
timers/listeners не накапливаются
modal/dropdown не остаются после unmount
41. Для CSS изменений
Дополнительно проверить:
не появился новый global reset
не появился новый late override layer
не вырос !important без причины
shared component не получил feature dependency
feature не стал владельцем generic component surface
42. Для button изменений
Дополнительно проверить:
producer classes
ancestor context
normal
hover
active
focus-visible
disabled
pseudo-elements
currentColor
43. Запрещённые быстрые решения
Если агент собирается сделать что-то из следующего — нужно остановиться и пересмотреть решение:
добавить ещё один !important
создать fixes.css
создать legacy override
поставить CSS-файл последним
добавить button:not(...)
перерисовать page через self replaceWith
создать второй TopBar
создать второй Dropdown
копировать shared component в feature
Обычно это признак неправильного ownership.
44. Search-before-add
Перед созданием:
нового class
нового component
нового helper
нового CSS rule
сначала искать аналог по проекту.
Не создавать:
channel-menu-v2
channel-menu-new
topbar-alt
button-final
avatar-new
если shared abstraction уже существует.
45. Не сохранять мёртвый styling
Если historical declaration не была observable в текущем accepted baseline и новый architecture делает её visible — не защищать её «на всякий случай».
Удалить dead styling.
Accepted visual behavior важнее исторического текста CSS.
46. Не менять accepted baseline незаметно
Если задача требует намеренного визуального изменения, это должно быть явно указано.
Архитектурный refactor по умолчанию означает:
behavior before
=
behavior after
Если parity нельзя сохранить — сообщить об этом отдельно.
47. Комментарии и документация
Комментарии должны объяснять:
WHY
а не очевидное:
WHAT
Хорошо:
// Ignore stale refresh completion after a newer refresh or page disposal.
Плохо:
// Increment variable.
refreshSeq += 1;
48. Не использовать архитектурные документы как мусорный лог
AGENTS.md содержит постоянные правила.
Stage-specific audit/report должен быть отдельным документом.
Не добавлять сюда:
временные баги
результаты одного запуска
конкретные debug logs
временные TODO
если это не постоянное правило.
49. Definition of Done для UI-изменения
Изменение считается готовым только если:
1. Функция работает.
2. Ownership понятен.
3. Старый architecture не возвращён.
4. Lifecycle не имеет второго owner.
5. CSS не получил новую specificity war.
6. Shared components не дублированы.
7. Async callbacks безопасны после unmount.
8. Проверки проходят.
9. Нет временных fixtures/debug code.
10. Diff не содержит unrelated refactor.
50. Если не уверен
Не компенсировать непонимание дополнительным override.
Сначала исследовать:
producer
owner
DOM
cascade
lifecycle
Правильный вопрос:
«какому существующему владельцу должно принадлежать это поведение?»
а не:
«какой selector сделать сильнее, чтобы заработало?»
Краткая памятка
Нельзя
components.css
fixes.css
late override layer
button:not(...) reset
новые visual !important
self render + replaceWith
второй TopBar
второй Dropdown
feature-owned generic Avatar
новый router для локальной задачи
Нужно
один owner
stable page root
local refresh
idempotent cleanup
async disposed guards
shared components
semantic buttons
feature layout/states
минимальная specificity
проверка реального DOM producer
Главный архитектурный принцип
Всегда предпочитать:
один владелец
+
явный contract
+
локальное состояние
вместо:
исторический override
+
случайный source order
+
ещё один fix
Если изменение требует нарушить это правило — сначала остановиться и обосновать архитектурное исключение.