feat: режим индикаторов без цветовой кодировки + переустройство /setup

Новая настройка app_settings.indicator_mode (color | accessible), переключается
на /setup. В режиме accessible сигналы, которые раньше держались только на цвете,
переходят на форму/текст:
- качество цены (0-9) — 5-ступенчатый signal-meter вместо градиентной точки/числа;
  единый модуль web/static/price-quality.js, ветвление по window.QF_INDICATOR_MODE;
- вкладка «Ценообразование»: ведущая колонка-meter вместо заливки строк,
  пометка W вместо амбер-подсветки world-цен, ⚠ вместо красного «загрязнённого» итога.
- GET/PUT /api/settings/ui (без рестарта), регистрируется в обоих наборах роутов.

/setup переустроен: две колонки одной высоты, кнопка «Вернуться в приложение»,
исправлен <title>.

Документация: bible-local 02/03/04 + decisions/2026-08-31-accessible-indicator-mode.md

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LbRvQgPZpM4SJTaX3iLrXk
This commit is contained in:
Mikhail Chusavitin
2026-08-31 18:08:38 +03:00
co-authored by Claude Sonnet 5
parent d97ac447ca
commit 681ec15e2b
15 changed files with 453 additions and 52 deletions
+39
View File
@@ -44,3 +44,42 @@
visibility: visible;
opacity: 1;
}
/* Colour-blind-safe price-quality indicator (mono mode). 5-step signal meter,
single neutral hue — magnitude reads from the number of lit bars, not colour. */
.qf-quality-meter {
display: inline-flex;
align-items: flex-end;
gap: 1px;
height: 12px;
vertical-align: middle;
line-height: 0;
}
.qf-quality-meter > i {
display: inline-block;
width: 3px;
background-color: #d1d5db;
border-radius: 1px;
}
.qf-quality-meter > i.on {
background-color: #374151;
}
.qf-quality-meter > i:nth-child(1) { height: 30%; }
.qf-quality-meter > i:nth-child(2) { height: 45%; }
.qf-quality-meter > i:nth-child(3) { height: 62%; }
.qf-quality-meter > i:nth-child(4) { height: 80%; }
.qf-quality-meter > i:nth-child(5) { height: 100%; }
.qf-quality-meter[data-size="sm"] {
height: 10px;
gap: 1px;
}
.qf-quality-meter[data-size="sm"] > i {
width: 2px;
}
/* Leading meter column on the pricing tab tables (mono mode). */
.pricing-quality-meter {
width: 1%;
white-space: nowrap;
text-align: center;
}
+44 -9
View File
@@ -1,16 +1,25 @@
// Shared LOT price-quality color scale, used everywhere price_quality is
// Shared LOT price-quality indicator, used everywhere price_quality is
// displayed (pricelist detail page, configurator search dropdown, configurator
// table, pricing tab). price_quality is a 0-9 score set by the external
// pricing tool (see bible-local/03-database.md, qt_pricelist_items.price_quality)
// — QF only displays it, never computes it. Single source of the color scale
// so every view stays visually consistent.
// table, pricing tab). price_quality is a 0-9 score set by the external pricing
// tool (see bible-local/03-database.md, qt_pricelist_items.price_quality) — QF
// only displays it, never computes it. Single source of the visual scale so
// every view stays consistent.
//
// Gradient: 0 = red, 5 = yellow, 9 = green.
// Two render modes, chosen per client via window.QF_INDICATOR_MODE (injected by
// base.html from app_settings, key indicator_mode):
// "color" (default) — hue scale, 0 = red, 5 = yellow, 9 = green.
// "accessible" — colour-blind-safe 5-step signal-strength meter,
// single neutral hue, magnitude encoded by bar count.
// See bible-local/decisions/2026-08-31-accessible-indicator-mode.md.
(function () {
const RED = [220, 38, 38];
const YELLOW = [234, 179, 8];
const GREEN = [22, 163, 74];
function isAccessible() {
return window.QF_INDICATOR_MODE === 'accessible';
}
function lerp(c1, c2, t) {
return c1.map((v, i) => Math.round(v + (c2[i] - v) * t));
}
@@ -28,27 +37,53 @@
return rgb ? `rgb(${rgb})` : null;
}
// Small colored dot for compact contexts (search dropdown, configurator table cell).
// 0-9 score -> 0-4 meter level. 0-1 / 2-3 / 4-5 / 6-7 / 8-9.
function priceQualityLevel(quality) {
if (typeof quality !== 'number' || Number.isNaN(quality)) return null;
const q = Math.max(0, Math.min(9, quality));
return Math.min(4, Math.floor(q / 2));
}
// Monochrome 5-step signal-strength meter. size: undefined | 'sm'.
function qualityMeterHtml(quality, opts) {
const level = priceQualityLevel(quality);
if (level === null) return '';
const size = opts && opts.size === 'sm' ? ' data-size="sm"' : '';
const title = `Качество цены: ${quality}/9`;
let bars = '';
for (let i = 0; i < 5; i++) {
bars += `<i class="${i <= level ? 'on' : ''}"></i>`;
}
return `<span class="qf-quality-meter"${size} role="img" aria-label="${title}" title="${title}">${bars}</span>`;
}
// Small indicator for compact contexts (search dropdown, configurator table cell).
function qualityDotHtml(quality) {
if (isAccessible()) return qualityMeterHtml(quality, { size: 'sm' });
const color = priceQualityColor(quality);
if (!color) return '';
return `<span class="inline-block w-2 h-2 rounded-full" style="background-color: ${color}" title="Качество цены: ${quality}/9"></span>`;
}
// Colored number badge for a dedicated "quality" column/cell.
// Indicator for a dedicated "quality" column/cell.
function qualityBadgeHtml(quality) {
if (typeof quality !== 'number') return '<span class="text-gray-400">-</span>';
if (isAccessible()) return qualityMeterHtml(quality, { size: 'sm' });
const color = priceQualityColor(quality);
return `<span class="font-semibold" style="color: ${color}" title="Качество цены: ${quality}/9">${quality}</span>`;
}
// Light row-tint background for table rows, e.g. the pricing tab.
// Light row-tint background for table rows, e.g. the pricing tab. Suppressed
// in accessible mode — the pricing tab carries its own leading meter column there.
function qualityRowStyle(quality) {
if (isAccessible()) return '';
const rgb = priceQualityRgb(quality);
return rgb ? `background-color: rgba(${rgb}, 0.10)` : '';
}
window.priceQualityColor = priceQualityColor;
window.priceQualityLevel = priceQualityLevel;
window.qualityMeterHtml = qualityMeterHtml;
window.qualityDotHtml = qualityDotHtml;
window.qualityBadgeHtml = qualityBadgeHtml;
window.qualityRowStyle = qualityRowStyle;