Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 65 additions & 14 deletions crates/labcolors-core/src/cleanliness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,67 @@ pub enum Theme {
DarkIc,
}

impl Theme {
/// Разобрать стабильный kebab-контракт границы (`"light"` / `"dark"` /
/// `"light-ic"` / `"dark-ic"`). Неизвестная строка — ошибка вызывающего,
/// возвращается как есть (граница оборачивает в свой тип ошибки), никогда
/// не коэрсится в тему по умолчанию.
///
/// # Errors
///
/// `Err` с непринятой строкой.
pub fn parse(raw: &str) -> Result<Self, String> {
match raw {
"light" => Ok(Theme::Light),
"dark" => Ok(Theme::Dark),
"light-ic" => Ok(Theme::LightIc),
"dark-ic" => Ok(Theme::DarkIc),
other => Err(other.to_string()),
}
}

/// Стабильный kebab-ключ темы — обратная к [`parse`](Self::parse).
pub fn key(self) -> &'static str {
match self {
Theme::Light => "light",
Theme::Dark => "dark",
Theme::LightIc => "light-ic",
Theme::DarkIc => "dark-ic",
}
}

/// Условия просмотра, под которыми ядро резолвит эту тему: та же карта
/// surround-ов, что у [`vc_for_context`] (Light → average, Dark → dim,
/// IC-темы → high-contrast двойники), но с дефолтным Yb — вход границы,
/// где фон ещё неизвестен.
pub fn viewing_conditions(self) -> crate::spaces::vc::ViewingConditions {
self.vc_by(
crate::spaces::vc::ViewingConditions::srgb,
crate::spaces::vc::ViewingConditions::dim_surround,
)
}

/// ЕДИНАЯ карта тема → surround: light-темы берут average-конструктор,
/// dark-темы — dim, IC-темы поднимают флаг повышенного контраста. Обе
/// точки входа (дефолтный Yb на границе, Yb-от-фона в контексте дефектов)
/// обязаны выбирать surround здесь — вторая копия карты в цветовом коде
/// расходилась бы тихо при добавлении темы.
fn vc_by(
self,
srgb: impl FnOnce() -> crate::spaces::vc::ViewingConditions,
dim: impl FnOnce() -> crate::spaces::vc::ViewingConditions,
) -> crate::spaces::vc::ViewingConditions {
let (mut vc, ic) = match self {
Theme::Light => (srgb(), false),
Theme::Dark => (dim(), false),
Theme::LightIc => (srgb(), true),
Theme::DarkIc => (dim(), true),
};
vc.high_contrast = vc.high_contrast || ic;
vc
}
}

/// Контекст просмотра для surround-aware оценки дефектов.
///
/// Сочетает фон (hex-строка, задаёт яркость Yb) и тему (определяет surround).
Expand Down Expand Up @@ -448,20 +509,10 @@ fn y_pct_from_hex(hex: &str) -> Result<f64, String> {
///
/// Параметры surround — CIECAM16 Table 1 (Li et al. 2017). Ноль новых констант.
fn vc_for_context(theme: Theme, y_b_pct: f64) -> crate::spaces::vc::ViewingConditions {
match theme {
Theme::Light => crate::spaces::vc::ViewingConditions::srgb_with_yb(y_b_pct),
Theme::Dark => crate::spaces::vc::ViewingConditions::dim_surround_with_yb(y_b_pct),
Theme::LightIc => {
let mut vc = crate::spaces::vc::ViewingConditions::srgb_with_yb(y_b_pct);
vc.high_contrast = true;
vc
}
Theme::DarkIc => {
let mut vc = crate::spaces::vc::ViewingConditions::dim_surround_with_yb(y_b_pct);
vc.high_contrast = true;
vc
}
}
theme.vc_by(
|| crate::spaces::vc::ViewingConditions::srgb_with_yb(y_b_pct),
|| crate::spaces::vc::ViewingConditions::dim_surround_with_yb(y_b_pct),
)
}

/// Surround-aware оценка грязи цвета в заданном контексте просмотра.
Expand Down
19 changes: 15 additions & 4 deletions crates/labcolors-core/src/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -993,10 +993,7 @@ impl RoleTable {
/// against the live background. The value is a property of the contract,
/// not of any one solve, so it is exposed alongside each resolved role.
pub fn legal_floor(&self, role: Role) -> Option<f64> {
match self.spec(role) {
RoleSpec::Anchor(anchor) => anchor.conformance().min_ratio(),
_ => None,
}
self.spec(role).legal_floor()
}

/// Return a copy with `role`'s recipe replaced — every other role keeps its
Expand Down Expand Up @@ -1739,6 +1736,20 @@ pub struct NamedRoleTable {
chroma: RoleChroma,
}

impl RoleSpec {
/// WCAG-пол этой спеки — свойство контракта, не резолва: текст/UI-якорь
/// несёт пол своего [`TextAnchor`] (AaText → 4.5, AaUi → 3.0), все
/// остальные формы (декоративные, dJ', лестница, альфа-аналог, zero) —
/// без легального пола. Одна семантика для обеих таблиц
/// ([`RoleTable::legal_floor`] и string-keyed границы).
pub fn legal_floor(&self) -> Option<f64> {
match self {
RoleSpec::Anchor(anchor) => anchor.conformance().min_ratio(),
_ => None,
}
}
}

impl NamedRoleTable {
/// Build a named table from its `(name, recipe)` entries and an undertone
/// policy. Names are the CSS contract downstream (`--lab-{name}`); this
Expand Down
5 changes: 5 additions & 0 deletions crates/labcolors-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ js-sys = "0.3"
# Derive-only: thiserror is a proc-macro with no runtime code, so it adds
# nothing to the WASM bundle while giving matchable, well-described errors.
thiserror = "2"
# Граница конфига: JSON живёт ТОЛЬКО в этом крейте (ядро — ноль
# runtime-зависимостей). Размер бандла отслеживается CI-шагом report bundle
# size (информационный до perf-bench).
serde = { version = "1", features = ["derive"] }
serde_json = "1"

[dev-dependencies]
wasm-bindgen-test = "0.3"
Expand Down
8 changes: 8 additions & 0 deletions crates/labcolors-wasm/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ impl<V: Clone> ContractCache<V> {
value
}

/// Очистить кэш целиком. Смена таблицы (загрузка конфига) обязана снести
/// прошлое пространство записей: одновременно в кэше живёт ровно ОДНО
/// пространство ключей, и корректность не опирается на вероятностную
/// уникальность отпечатка.
pub fn clear(&self) {
self.entries.borrow_mut().clear();
}

/// Number of live entries — for tests and introspection.
#[cfg(test)]
pub fn len(&self) -> usize {
Expand Down
Loading