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>
152 lines
6.8 KiB
Python
152 lines
6.8 KiB
Python
"""
|
||
5 colour-scheme variants for v8 — bigger sidebar/main contrast.
|
||
Constraint: brand accent Teal #0F6E56 не оспариваем (брендбук v1.1).
|
||
Each variant fully WCAG-AA verified: ink/bg, ink/surface, ink/sidebar (text on
|
||
sidebar bg), accent/bg, white/sidebar-active, sidebar/bg (≥3:1 чтобы visually
|
||
"выделить левую часть").
|
||
"""
|
||
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(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)
|
||
return colour.cctf_encoding(np.clip(rgb_lin, 0, 1), function='sRGB')
|
||
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)
|
||
|
||
WHITE = np.array([1.0,1.0,1.0])
|
||
ACCENT = hex_rgb('#0F6E56') # Teal — брендбук v1.1, не оспаривается
|
||
# Bright-Teal для icon-on-dark контекста (Teal слишком тёмный для тёмного sidebar)
|
||
ACCENT_BRIGHT = oklch(0.75, 0.13, 175)
|
||
print(f"# accent-bright (icon on dark sidebar): {srgb_hex(ACCENT_BRIGHT)}")
|
||
print()
|
||
|
||
# 5 variants — each defined by (sidebar bg, sidebar text, sidebar active fill,
|
||
# page bg, surface, ink, ink-3, optional secondary accent)
|
||
variants = []
|
||
|
||
# ───── Вариант А — Graphite Sidebar (Linear/Vercel-grade)
|
||
variants.append({
|
||
'name': 'А · Graphite Sidebar',
|
||
'desc': 'Тёмный графитовый sidebar + холодный near-white main. Самый «технологичный» из всех — линия Linear/Vercel. Контраст левая/правая = максимальный.',
|
||
'sidebar': oklch(0.205, 0.012, 240), # near-black cool
|
||
'sidebar_text': oklch(0.78, 0.005, 240),
|
||
'sidebar_act': oklch(0.30, 0.015, 240),
|
||
'bg': oklch(0.965, 0.005, 240), # cooler than v7
|
||
'surface': WHITE,
|
||
'sunken': oklch(0.945, 0.006, 240),
|
||
'hairline': oklch(0.88, 0.006, 240),
|
||
'ink': oklch(0.18, 0.018, 240),
|
||
'ink_3': oklch(0.50, 0.012, 240),
|
||
})
|
||
|
||
# ───── Вариант Б — Forest Sidebar (бренд-родной)
|
||
variants.append({
|
||
'name': 'Б · Forest Sidebar',
|
||
'desc': 'Глубокий тёмно-тиловый sidebar (родственник брендового Teal) + тёплый ivory main. Брендовая связность, корпоративная фундаментальность.',
|
||
'sidebar': oklch(0.22, 0.040, 175), # deep teal-noir
|
||
'sidebar_text': oklch(0.80, 0.020, 175),
|
||
'sidebar_act': oklch(0.31, 0.045, 175),
|
||
'bg': oklch(0.965, 0.010, 90), # warm ivory
|
||
'surface': oklch(0.995, 0.005, 90),
|
||
'sunken': oklch(0.945, 0.012, 90),
|
||
'hairline': oklch(0.875, 0.012, 85),
|
||
'ink': oklch(0.18, 0.020, 230),
|
||
'ink_3': oklch(0.50, 0.012, 90),
|
||
})
|
||
|
||
# ───── Вариант В — Slate-Blue Financial
|
||
variants.append({
|
||
'name': 'В · Slate-Blue Financial',
|
||
'desc': 'Sidebar — глубокий синий слейт, main — холодный mist. Stripe Atlas/Mercury вайб, финансовый инструмент.',
|
||
'sidebar': oklch(0.235, 0.030, 250), # deep navy slate
|
||
'sidebar_text': oklch(0.80, 0.015, 250),
|
||
'sidebar_act': oklch(0.34, 0.035, 250),
|
||
'bg': oklch(0.945, 0.010, 240), # cool mist
|
||
'surface': WHITE,
|
||
'sunken': oklch(0.92, 0.012, 240),
|
||
'hairline': oklch(0.86, 0.014, 240),
|
||
'ink': oklch(0.18, 0.020, 245),
|
||
'ink_3': oklch(0.50, 0.014, 245),
|
||
})
|
||
|
||
# ───── Вариант Г — Warm Champagne
|
||
variants.append({
|
||
'name': 'Г · Warm Champagne',
|
||
'desc': 'Sidebar — насыщенный кремово-бежевый, main — pure white. Тёплый «кабинет», Notion/Things с строгостью.',
|
||
'sidebar': oklch(0.92, 0.025, 80), # rich champagne
|
||
'sidebar_text': oklch(0.32, 0.015, 80),
|
||
'sidebar_act': oklch(0.18, 0.020, 240), # ink fill
|
||
'bg': WHITE,
|
||
'surface': oklch(0.985, 0.005, 80),
|
||
'sunken': oklch(0.94, 0.018, 80),
|
||
'hairline': oklch(0.86, 0.022, 80),
|
||
'ink': oklch(0.18, 0.020, 240),
|
||
'ink_3': oklch(0.48, 0.012, 80),
|
||
})
|
||
|
||
# ───── Вариант Д — Inverted Hero (cards float)
|
||
variants.append({
|
||
'name': 'Д · Inverted Hero',
|
||
'desc': 'Sidebar — pure white, main — насыщенный cool grey. Карточки «плавают» над полотном, классика премиум B2B (Stripe Dashboard).',
|
||
'sidebar': WHITE,
|
||
'sidebar_text': oklch(0.32, 0.014, 240),
|
||
'sidebar_act': oklch(0.18, 0.020, 240), # ink fill
|
||
'bg': oklch(0.91, 0.012, 240), # noticeably cool grey
|
||
'surface': WHITE,
|
||
'sunken': oklch(0.88, 0.013, 240),
|
||
'hairline': oklch(0.82, 0.015, 240),
|
||
'ink': oklch(0.18, 0.020, 245),
|
||
'ink_3': oklch(0.48, 0.013, 245),
|
||
})
|
||
|
||
# Compute & print
|
||
for v in variants:
|
||
print("=" * 84)
|
||
print(v['name'])
|
||
print(v['desc'])
|
||
print("-" * 84)
|
||
print(f" sidebar {srgb_hex(v['sidebar'])}")
|
||
print(f" sidebar text {srgb_hex(v['sidebar_text'])}")
|
||
print(f" sidebar act {srgb_hex(v['sidebar_act'])}")
|
||
print(f" bg {srgb_hex(v['bg'])}")
|
||
print(f" surface {srgb_hex(v['surface'])}")
|
||
print(f" sunken {srgb_hex(v['sunken'])}")
|
||
print(f" hairline {srgb_hex(v['hairline'])}")
|
||
print(f" ink {srgb_hex(v['ink'])}")
|
||
print(f" ink-3 {srgb_hex(v['ink_3'])}")
|
||
print(f" accent (teal, fixed) #0F6E56")
|
||
print()
|
||
print(" WCAG checks:")
|
||
pairs = [
|
||
('ink / bg', v['ink'], v['bg'], 4.5),
|
||
('ink / surface', v['ink'], v['surface'], 4.5),
|
||
('ink-3 / bg', v['ink_3'], v['bg'], 4.5),
|
||
('sidebar text / sidebar', v['sidebar_text'], v['sidebar'], 4.5),
|
||
('white / sidebar-active', WHITE, v['sidebar_act'], 4.5),
|
||
('accent / bg', ACCENT, v['bg'], 4.5),
|
||
('accent-bright / sidebar (icon)', ACCENT_BRIGHT, v['sidebar'], 3.0),
|
||
('SIDEBAR vs BG (layout div)', v['sidebar'], v['bg'], 3.0),
|
||
]
|
||
for name, a, b, target in pairs:
|
||
r = cr(a, b)
|
||
flag = '✓' if r >= target else 'FAIL'
|
||
print(f" {name:<32} {r:6.2f}:1 ≥{target} {flag}")
|
||
print()
|