This commit is contained in:
2026-10-14 23:39:28 +07:00
parent 734025a765
commit 2ed1bf377b
4 changed files with 127 additions and 14 deletions
+13
View File
@@ -343,3 +343,16 @@
- Репозиторий: `evening_detective_frontend`
- Результат: добавлено создание игры из каталога сценариев, выбор даты и времени, отображение даты и исправлена передача RFC 3339 `startAt`.
- Проверки: type-check, lint и lint:css пройдены; build указан в отчёте плана.
### ED-024 — Общая дизайн-система фронтенда и библиотека компонентов
- Статус: `DONE`
- Дата: 14.10.2026
- Репозиторий: `evening_detective_frontend`
- Результат: формализована дизайн-система; по утверждённому плану выделены SiteThemeProvider, CaseFolder, CaseBoard, ObjectCover, StatusStamp, TextHighlight, SectionHeading и DossierFooter; сохранён общий DesignBrand. Образец сайта использует библиотеку, игровые компоненты включены без переработки. Добавлена dev-вкладка «Компоненты» с прямыми импортами, props, slots, событиями и локальными примерами Naive UI; сайт открывается первым, стрелки/Home/End работают для трёх вкладок.
- Поведение: данные обложек и доски передаются родителем; status необязателен и не управляет disabled. Цитаты повторных карточек получают уникальные useId и раскрываются независимо; open обрабатывается локально. Тема сайта изолирована от игры; основные цвета и reduced motion сохранены. В игровом образце добавлен отступ 6 px для выступающих металлических краёв панели.
- Документация: DESIGN_SYSTEM.md дополнен подключением provider и таблицей контрактов. Повторяемый smoke: tests/venue/design_system.py (Vite на 5198, Firefox), инструкция в tests/venue/README.md.
- Проверка: type-check, lint, lint:css, npm run build и git diff --check — успешно. Firefox: 375/768/1280 px, три вкладки, клавиатура, отсутствие переполнения внутренней прокрутки, разные данные, независимые цитаты, уникальность ID, optional status/disabled/loading, форма, окно и возврат фокуса, open, локальный игровой ход, темы, reduced motion. Просмотрены контрольные снимки и вручную проверен diff по FRONTEND_CODE_STANDARD.md. Проверены production-файлы: dev-маршрут и каталог исключены.
- Ограничения: рабочие страницы не мигрировали, игровые компоненты не унифицировали, зависимости не добавляли; commit/push не выполняли. Браузерные проверки выполнены в Firefox, другие браузеры не проверялись.
- Уточнение ED-024: каталог перестроен в последовательные секции «заголовок → живой компонент → описание → код использования» одной колонкой; игровые и Naive UI примеры расположены рядом со своим кодом. Сохранены изоляция тем и локальные действия. Повторно пройдены type-check, lint, lint:css, сборка и Firefox smoke на 375/768/1280 px; diff проверен по стандарту.
-14
View File
@@ -98,20 +98,6 @@
- Не входит: разработка рассылки и статистики, автоматизация всей очистки, налоговые вопросы, регистрация ИП/ООО, commit/push.
- Следующий шаг: уточнить у владельца подачу уведомления и данные для него, проверить условия Gmail / Яндекс и фактические копии данных; затем определить исполнимые процедуры и согласовать вывод о запуске.
### ED-024 — Общая дизайн-система фронтенда
- Статус: `TODO`
- Приоритет: `P1`
- Репозиторий: `evening_detective_frontend`
- Цель: создать единый источник правил, по которому можно последовательно делать новые экраны, компоненты и другие визуальные материалы.
- Контекст: во фронтенде уже есть визуальный язык на основе `base.css`, `main.css`, бумажных и металлических текстур, шрифта `a_OldTyper`, собственных компонентов и `naive-ui`; его нужно формализовать без смены общего характера.
- Решение: создать в фронтенде документ дизайн-системы и CSS-токены в существующем стиле проекта; описать цвета, типографику, размеры, отступы, сетку, адаптивность, поверхности, текстуры, состояния, фокус и доступность; зафиксировать правила для кнопок, полей, карточек, панелей, списков, таблиц, модальных окон, уведомлений, навигации, загрузки и ошибок; описать правила иконок, изображений, иллюстраций, именования и примеры правильного использования.
- Критерии: по системе можно без дополнительных устных решений спроектировать новый экран и написать его Vue/CSS-код; токены имеют однозначные имена и назначение; описаны основные состояния и доступность; правила согласуются с текущим визуальным языком; существующий фронтенд не меняет поведение и внешний вид в рамках этой задачи.
- Ограничения: не менять существующие страницы и компоненты; не делать редизайн приложения; не добавлять зависимости; переиспользовать текущие стили, ассеты и `naive-ui`; не создавать Figma-файл или полноценную отдельную библиотеку компонентов.
- Не входит: миграция текущих экранов на новые токены; реализация всех описанных компонентов; изменение API и продуктовой логики.
- Проверка: проверить полноту и однозначность документа и CSS-токенов; выполнить `cd evening_detective_frontend && npm run type-check && npm run lint && npm run lint:css`.
- Следующий шаг: изучить текущие `base.css`, `main.css`, стили и повторяющиеся паттерны компонентов, затем подготовить минимальный документ и набор токенов без миграции существующих экранов.
### ED-018 — Удаление неиспользуемых пользовательских файлов
- Статус: `TODO`
+7
View File
@@ -210,3 +210,10 @@ TR3000 V1 2.5.27-20260804-183625 без изменения настроек ро
Flow также проверяет переход заполненной БД с числовых ID команд на UUID v7,
сохранность данных и ограничений, независимое создание команд в трёх схемах
и полную Go-проверку на временном PostgreSQL. `team_uuid.py` вызывается из flow.
## Дизайн-система (без сервера и БД)
Запустить фронтенд командой `npm run dev -- --port 5198`, затем из корня
`python3 tests/venue/design_system.py`. Требуется установленный Firefox.
Smoke проверяет три ширины, клавиатурные вкладки, повторные карточки и ID,
локальную форму и окно, состояния, темы и reduced motion.
+107
View File
@@ -0,0 +1,107 @@
"""Run with Vite on port 5198 and Firefox installed; no API required."""
import json, pathlib, socket, subprocess, tempfile, time
profile=pathlib.Path(tempfile.mkdtemp(prefix='home-dossier-'))
with socket.socket() as s:
s.bind(('127.0.0.1',0));port=s.getsockname()[1]
(profile/'user.js').write_text(f'user_pref("marionette.port", {port});\nuser_pref("browser.shell.checkDefaultBrowser", false);')
log=(profile/'firefox.log').open('w')
firefox=subprocess.Popen(['firefox','--headless','--no-remote','--profile',str(profile),'--marionette','--remote-allow-system-access','about:blank'],stdout=log,stderr=log)
sock=None
try:
for _ in range(100):
try: sock=socket.create_connection(('127.0.0.1',port),timeout=1);break
except OSError: time.sleep(.1)
assert sock is not None, 'Firefox did not start'
sock.settimeout(20)
def read():
size=b''
while not size.endswith(b':'): size+=sock.recv(1)
data=b'';length=int(size[:-1])
while len(data)<length: data+=sock.recv(length-len(data))
return json.loads(data)
read();counter=0
def command(name,args=None):
global counter
counter+=1;payload=json.dumps([0,counter,name,args or {}]).encode()
sock.sendall(str(len(payload)).encode()+b':'+payload)
result=read();assert not result[2], result[2];return result[3]
def js(script):
result=command('WebDriver:ExecuteScript',{'script':script,'args':[],'newSandbox':False,'sandbox':None})
return result.get('value') if isinstance(result,dict) and 'value' in result else result
def wait(script):
for _ in range(100):
if js('return '+script):return
time.sleep(.1)
raise AssertionError(script)
command('WebDriver:NewSession',{'capabilities':{'alwaysMatch':{}}})
command('Marionette:SetContext', {'value':'chrome'})
js('document.documentElement.style.minWidth="0";document.documentElement.setAttribute("minwidth","0");return true')
command('Marionette:SetContext', {'value':'content'})
for width in [375,768,1280]:
command('WebDriver:SetWindowRect',{'width':width,'height':900})
command('WebDriver:Navigate',{'url':'http://127.0.0.1:5198/design-system'})
wait('!!document.querySelector(".site-preview")')
assert js('return document.querySelectorAll("[role=tab]").length')==3
assert js('return document.documentElement.scrollWidth <= innerWidth')
# Third tab, circular arrows and Home/End use real keyboard actions.
def key(value):
command('WebDriver:PerformActions', {'actions':[{'type':'key','id':'keyboard','actions':[{'type':'keyDown','value':value},{'type':'keyUp','value':value}]}]})
js('document.querySelector("#tab-site").focus();return true')
key('\ue010')
wait('!!document.querySelector(".catalog")')
assert js('return document.activeElement.id')=='tab-components'
assert js('return document.documentElement.scrollWidth <= innerWidth && document.querySelector(".design-scroll").scrollWidth <= innerWidth'), 'Catalog overflow'
assert js('const entries=[...document.querySelectorAll(".catalog-entry")]; return entries.length===20 && entries.every(e=>e.children[0].tagName==="H2" && e.children[1].matches(".component-sample, .n-config-provider") && e.children[3].matches("pre") && e.children[3].querySelector("code"))'), 'Each component needs a heading, sample and usage code'
assert js('return [...document.querySelectorAll("[id]")].map(e=>e.id).length===new Set([...document.querySelectorAll("[id]")].map(e=>e.id)).size'), 'Duplicate DOM IDs'
assert js('return document.querySelectorAll(".catalog .status-stamp").length===2'), 'Optional folder status'
assert js('return !document.querySelectorAll(".catalog .folder-action")[0].disabled && document.querySelectorAll(".catalog .folder-action")[1].disabled'), 'Status controls availability'
ids=js('return [...document.querySelectorAll(".catalog .clue-toggle")].map(e=>e.getAttribute("aria-controls"))')
assert len(set(ids))==2
js('document.querySelectorAll(".catalog .clue-toggle")[0].click();return true')
wait('document.querySelectorAll(".catalog .clue-toggle")[0].getAttribute("aria-expanded")==="true"')
assert js('return document.querySelectorAll(".catalog .clue-toggle")[1].getAttribute("aria-expanded")')=='false'
js('document.querySelectorAll(".catalog .clue-toggle")[1].click();return true')
wait('document.querySelectorAll(".catalog .clue-toggle")[1].getAttribute("aria-expanded")==="true"')
js('document.querySelectorAll(".catalog .clue-toggle")[0].click();return true')
wait('document.querySelectorAll(".catalog .clue-toggle")[0].getAttribute("aria-expanded")==="false"')
assert js('return document.querySelectorAll(".catalog .clue-toggle")[1].getAttribute("aria-expanded")')=='true'
assert js('return document.querySelector(".catalog .case-board").textContent.includes("Найден у двери")')
assert js('return document.querySelector(".catalog [data-component=ObjectCover] .object-cover").textContent.includes("Чердак")')
js('document.querySelector(".catalog form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));return true')
wait('document.querySelector(".catalog").textContent.includes("Укажите название команды")')
js('const e=document.querySelector("#catalog-team");e.value="Сыщики";e.dispatchEvent(new Event("input",{bubbles:true}));return true')
js('document.querySelector(".catalog form").dispatchEvent(new Event("submit",{bubbles:true,cancelable:true}));return true')
wait('document.querySelector(".catalog").textContent.includes("Сыщики» готова")')
js('document.querySelector(".catalog .folder-action").focus();document.querySelector(".catalog .folder-action").click();return true')
wait('!!document.querySelector("[role=dialog]")')
assert js('return document.querySelector("[role=dialog]").getAttribute("aria-label")')=='Повторное дело'
key('\ue00c')
wait('!document.querySelector("[role=dialog]")')
wait('document.activeElement===document.querySelector(".catalog .folder-action")')
assert js('return document.querySelector(".catalog .buttons button[disabled]")!==null && document.querySelector(".catalog .buttons .n-button--loading")!==null')
assert js('return getComputedStyle(document.querySelector(".catalog .buttons button")).backgroundColor')=='rgb(255, 212, 59)'
js('const e=document.querySelector(".game-input-run");e.value="Сад";e.dispatchEvent(new Event("input",{bubbles:true}));return true')
wait('!document.querySelector(".game-button-run").disabled')
js('document.querySelector(".game-button-run").click();return true')
wait('!!document.querySelector(".action-result")')
assert js('return getComputedStyle(document.querySelector(".action-result")).getPropertyValue("--n-icon-color").trim()')!='#ffd43b'
js('document.querySelector("#tab-components").focus();return true')
key('\ue014'); wait('!!document.querySelector(".site-preview")')
key('\ue012'); wait('!!document.querySelector(".catalog")')
key('\ue011'); wait('!!document.querySelector(".site-preview")')
key('\ue014'); wait('!!document.querySelector(".game-preview")')
key('\ue014'); wait('!!document.querySelector(".catalog")')
key('\ue011'); wait('!!document.querySelector(".site-preview")')
print(f'PASS {width}px: three tabs, quotes, IDs, data, states, form, open, modal, themes', flush=True)
command('Marionette:SetContext', {'value':'chrome'})
js('Services.prefs.setIntPref("ui.prefersReducedMotion",1);return true')
command('Marionette:SetContext', {'value':'content'})
wait('matchMedia("(prefers-reduced-motion: reduce)").matches')
js('document.querySelector(".clue-toggle").focus();return true')
assert js('return getComputedStyle(document.querySelector(".case-folder")).transform')=='none'
assert js('return getComputedStyle(document.querySelector(".case-folder")).transitionDuration')=='0s'
assert js('return getComputedStyle(document.querySelector(".clues"),"::before").transitionDuration')=='0s'
print('PASS reduced motion', flush=True)
finally:
if sock:sock.close()
firefox.terminate();firefox.wait(timeout=10);log.close()