887abf444e
Получен handoff-пакет liderra_v8_handoff/ от дизайнера Платона (kpd9363@gmail.com) от 07.05.2026 — v8 Forest. Заказчик 08.05 решил применить только в части дизайна, имени, логотипа. Функционал, состав страниц и правила (CTO-11, click-wrap, SSO break-glass, 14 статусов воронки) — без изменений (источник — ТЗ v8.5/schema v8.5). Что сделано: - Массовая замена Лидпоток→Лидерра (с учётом падежей: Лидерры/Лидерре) в 33 файлах (449 вхождений) — все .md/.sql/.json/.toml/.yml/.txt/.html, кроме исторических упоминаний внутри liderra_v8_handoff/ - Удалён docs/brandbook.md v1.1 — заменён на BRANDBOOK_v2.md из handoff - Скопированы 13 концептов liderra_v8_handoff/concepts/v8_*.html в web/v8/. Удалены старые web/01-login.html, 02-dashboard.html, 03-deals.html, index.html (палитра v1.1 deprecated) - CLAUDE.md v1.0→v1.1: §0 (BRANDBOOK_v2 + DEVELOPER_HANDOFF в источниках), §2 (палитра Forest, Inter+JBM, Lucide), §5 п.6 (anti-pattern Inter снят — в Forest Inter наш основной шрифт), §6 (13 концептов в web/v8/) - Реестр Открытые_вопросы_v8_3.md v1.12→v1.13: добавлена запись о ребрендинге + 4 точечных расхождений handoff vs ТЗ (статусы воронки, click-wrap чекбоксы, SSO fallback, axe violations) - package.json/package-lock.json: name lidpotok→liderra 4 расхождения handoff vs ТЗ (НЕ применены, источник истины — ТЗ/schema): 1. 14 «обобщённых» статусов в BRANDBOOK_v2 §3.6 ≠ 14 slug'ов в schema.sql:2076 (совпадает 2 из 14: «Переговоры», «Оплачено»). Источник — schema/ТЗ §6.4 (реселлерская модель из аудита crm.bp-gr.ru, 6 системных + 8 настраиваемых статусов). 2. 3-й click-wrap в v8_login.html («маркетинг-опционально») ≠ ТЗ §1.5/§4.1 («согласие на ПДн», обязательное, OPEN-Ж-3). 3. SSO в v8_admin.html («локальный 2FA fallback») ≠ ТЗ OPEN-И-13 (break-glass super_admin, локальный 2FA выключен). 4. Заявление «axe-core 4.10.2 — 0 violations» в README handoff — локально Pa11y 9.1.1 + axe нашёл 81 violation на 10/13 HTML (преимущественно color-contrast на декоративных separator'ах с --ink-disabled). Чисто: settings/errors/palette_options. Что НЕ включено в коммит: - лендинг/TZ_landing_v1_0.md — untracked, не моя работа в этой сессии - .tmp/ — gitignored Что осталось (для следующих сессий): - Возможное переименование GitHub-репо CoralMinister/lidpotok → liderra (отдельное решение заказчика) - Опционально: обратная связь Платону по 4 расхождениям handoff vs ТЗ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""v7 palette — cool-tech light theme. Proves WCAG AA before HTML write."""
|
|
import colour
|
|
import numpy as np
|
|
|
|
def hex_rgb(h):
|
|
h = h.lstrip("#")
|
|
return np.array([int(h[i:i+2], 16)/255 for i in (0,2,4)])
|
|
def srgb_hex(rgb):
|
|
rgb = np.clip(rgb, 0, 1)
|
|
return "#" + "".join(f"{round(c*255):02X}" for c in rgb)
|
|
def oklch_to_hex(L, C, H):
|
|
H = np.deg2rad(H)
|
|
Lab = np.array([L, C*np.cos(H), C*np.sin(H)])
|
|
XYZ = colour.Oklab_to_XYZ(Lab)
|
|
rgb_lin = colour.XYZ_to_sRGB(XYZ, apply_cctf_encoding=False)
|
|
rgb = colour.cctf_encoding(np.clip(rgb_lin, 0, 1), function='sRGB')
|
|
return srgb_hex(rgb), rgb
|
|
def lum(rgb):
|
|
rgb = np.clip(rgb, 0, 1)
|
|
rl = np.where(rgb <= 0.03928, rgb/12.92, ((rgb+0.055)/1.055)**2.4)
|
|
return float(0.2126*rl[0] + 0.7152*rl[1] + 0.0722*rl[2])
|
|
def cr(a, b):
|
|
L1, L2 = lum(a), lum(b)
|
|
if L1 < L2: L1, L2 = L2, L1
|
|
return (L1 + 0.05) / (L2 + 0.05)
|
|
|
|
# Cool-tech tokens — все вокруг hue=240 (нейтральный синеватый), чтобы поверхность читалась
|
|
# как "лабораторная" а не "тёплая бумага". Brand accent — Teal #0F6E56 из брендбука,
|
|
# его НЕ трогаем.
|
|
|
|
print("=" * 80)
|
|
print("v7 cool-tech tokens (Page surface — neutral cool, hue=240)")
|
|
print("=" * 80)
|
|
|
|
# Светлые поверхности
|
|
bg_hex, bg_rgb = oklch_to_hex(0.985, 0.003, 240) # page bg
|
|
surface_hex, surface_rgb = oklch_to_hex(1.000, 0.000, 240) # raised — pure white
|
|
sunken_hex, sunken_rgb = oklch_to_hex(0.970, 0.004, 240) # sunken
|
|
hairline_hex, hairline_rgb = oklch_to_hex(0.910, 0.005, 240) # hairline (UI border, not text)
|
|
|
|
# Текст
|
|
ink_hex, ink_rgb = oklch_to_hex(0.180, 0.018, 240) # primary text
|
|
ink2_hex, ink2_rgb = oklch_to_hex(0.350, 0.014, 240) # secondary
|
|
ink3_hex, ink3_rgb = oklch_to_hex(0.510, 0.012, 240) # muted (must AA on bg)
|
|
inkd_hex, inkd_rgb = oklch_to_hex(0.620, 0.008, 240) # disabled (3:1+)
|
|
|
|
# Brand accent (Teal #0F6E56) — НЕ пересчитываем, фиксируем из брендбука
|
|
accent_rgb = hex_rgb('#0F6E56')
|
|
|
|
palette = [
|
|
('bg', bg_hex, bg_rgb),
|
|
('surface', surface_hex, surface_rgb),
|
|
('sunken', sunken_hex, sunken_rgb),
|
|
('hairline', hairline_hex, hairline_rgb),
|
|
('ink', ink_hex, ink_rgb),
|
|
('ink-2', ink2_hex, ink2_rgb),
|
|
('ink-3', ink3_hex, ink3_rgb),
|
|
('ink-disabled',inkd_hex, inkd_rgb),
|
|
('accent (teal)','#0F6E56', accent_rgb),
|
|
]
|
|
for name, h, _ in palette:
|
|
print(f" {name:<14} {h}")
|
|
|
|
print()
|
|
print("WCAG verifications — text colour on bg/surface/sunken")
|
|
print("=" * 80)
|
|
checks = [
|
|
('ink / bg', ink_rgb, bg_rgb, 4.5),
|
|
('ink / surface', ink_rgb, surface_rgb, 4.5),
|
|
('ink / sunken', ink_rgb, sunken_rgb, 4.5),
|
|
('ink-2 / bg', ink2_rgb, bg_rgb, 4.5),
|
|
('ink-2 / sunken', ink2_rgb, sunken_rgb, 4.5),
|
|
('ink-3 / bg', ink3_rgb, bg_rgb, 4.5),
|
|
('ink-3 / surface', ink3_rgb, surface_rgb, 4.5),
|
|
('ink-3 / sunken', ink3_rgb, sunken_rgb, 4.5),
|
|
('ink-disabled / bg', inkd_rgb, bg_rgb, 3.0), # disabled — 3:1 OK
|
|
('accent / bg', accent_rgb, bg_rgb, 4.5),
|
|
('accent / surface',accent_rgb, surface_rgb, 4.5),
|
|
('white-on-accent (CTA text)', np.array([1,1,1]), accent_rgb, 4.5),
|
|
('white-on-ink (active nav)', np.array([1,1,1]), ink_rgb, 4.5),
|
|
('hairline / bg (3:1 UI)', hairline_rgb, bg_rgb, 3.0),
|
|
]
|
|
for name, a, b, target in checks:
|
|
r = cr(a, b)
|
|
flag = '✓' if r >= target else 'FAIL'
|
|
target_str = f"≥{target}"
|
|
print(f" {name:<35} {r:5.2f}:1 {target_str:>5} {flag}")
|