Icon packs are the owner's pick: the theme choice carries it, Appearance offers it
- ThemeChoice gains an optional icons field (absent: follow the theme's pack; site: the site's own icons; else a pack id), validated natively as a slug (waybetter-host theme.rs, with a test) and kept by per-theme choices; variants is an override group natively too. - Appearance: an Icons row (Follow the theme / the site's own icons / the site's packs) when the site lists packs; English words in the catalog. - The chrome's glyphs follow the owner's pick as well as the theme's suggestion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e88339bda6
commit
8a0ab6d3a1
8 changed files with 50 additions and 12 deletions
|
|
@ -42,7 +42,7 @@ pub const MAX_TOTAL_BYTES: usize = 4_000_000;
|
|||
/// Theme names: 1-60 characters (`theme.schema.json` `name`).
|
||||
pub const MAX_NAME_CHARS: usize = 60;
|
||||
/// Override groups the panel may change on top of a theme.
|
||||
pub const OVERRIDE_KEYS: [&str; 7] = ["font", "shape", "layout", "background", "effects", "colors", "css"];
|
||||
pub const OVERRIDE_KEYS: [&str; 8] = ["font", "shape", "layout", "background", "effects", "colors", "css", "variants"];
|
||||
|
||||
/// The theme applied to the site's tabs: a theme id (built-in or one of `themes`), an optional
|
||||
/// accent, per-group overrides from the panel and the owner's imported theme documents.
|
||||
|
|
@ -56,6 +56,10 @@ pub struct ThemeChoice {
|
|||
pub overrides: Map<String, Value>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub themes: Vec<Value>,
|
||||
/// The owner's icon pack: absent follows the theme's suggestion, `site` keeps the site's own icons,
|
||||
/// otherwise a pack id (theme engine v2). A slug; an unknown pack falls back to the site's icons in the page.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub icons: Option<String>,
|
||||
}
|
||||
|
||||
/// A theme id: a lowercase slug of 1-40 characters (`theme.schema.json` `id`).
|
||||
|
|
@ -75,7 +79,7 @@ fn doc_id(theme: &Value) -> Option<&str> {
|
|||
impl ThemeChoice {
|
||||
/// The site's own look, nothing customized.
|
||||
pub fn default_for(site: &ThemeSite) -> Self {
|
||||
Self { preset: site.own().into(), accent: None, overrides: Map::new(), themes: Vec::new() }
|
||||
Self { preset: site.own().into(), accent: None, overrides: Map::new(), themes: Vec::new(), icons: None }
|
||||
}
|
||||
|
||||
pub fn validate(&self, site: &ThemeSite) -> Result<(), String> {
|
||||
|
|
@ -84,6 +88,11 @@ impl ThemeChoice {
|
|||
return Err("VALIDATION: accent must use six-digit hex".into());
|
||||
}
|
||||
}
|
||||
if let Some(icons) = &self.icons {
|
||||
if !valid_id(icons) {
|
||||
return Err("VALIDATION: an icon pack id is a lowercase slug".into());
|
||||
}
|
||||
}
|
||||
if self.themes.len() > MAX_THEMES {
|
||||
return Err("VALIDATION: too many themes".into());
|
||||
}
|
||||
|
|
@ -120,7 +129,7 @@ impl ThemeChoice {
|
|||
/// True when the site keeps its own look (nothing to apply). Imported themes are kept in the
|
||||
/// file even while the site's own look is selected, so this only looks at what is applied.
|
||||
pub fn is_default(&self, site: &ThemeSite) -> bool {
|
||||
self.preset == site.own() && self.accent.is_none() && self.overrides.is_empty()
|
||||
self.preset == site.own() && self.accent.is_none() && self.overrides.is_empty() && self.icons.is_none()
|
||||
}
|
||||
|
||||
/// Nothing worth persisting: the site's own look and no imported themes.
|
||||
|
|
@ -139,7 +148,7 @@ impl ThemeChoice {
|
|||
let base = if id == self.preset || !self.knows(site, id) {
|
||||
self.clone()
|
||||
} else {
|
||||
ThemeChoice { preset: id.into(), accent: None, overrides: Map::new(), themes: self.themes.clone() }
|
||||
ThemeChoice { preset: id.into(), accent: None, overrides: Map::new(), themes: self.themes.clone(), icons: self.icons.clone() }
|
||||
};
|
||||
let preset = base.preset.clone();
|
||||
let themes = base.themes.into_iter().filter(|theme| doc_id(theme) == Some(preset.as_str())).collect();
|
||||
|
|
@ -304,6 +313,16 @@ mod tests {
|
|||
assert_eq!(imported.page_color(&SITE), Some((0xab, 0xcd, 0xef)), "an invalid page color falls back to bg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icon_choice_is_a_slug_and_counts_as_a_choice() {
|
||||
let picked = with(json!({ "preset": "site", "icons": "wb-pixel" })).unwrap();
|
||||
assert_eq!(picked.icons.as_deref(), Some("wb-pixel"));
|
||||
assert!(!picked.is_default(&SITE), "an icon pick is applied even on the site's own look");
|
||||
assert!(with(json!({ "preset": "site", "icons": "Bad Pack!" })).is_err());
|
||||
assert!(with(json!({ "preset": "site", "overrides": { "variants": { "nav": "top" } } })).is_ok(), "layout variants are an override group");
|
||||
assert_eq!(picked.for_theme(&SITE, "nord").icons.as_deref(), Some("wb-pixel"), "the icon pick is global");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_themes_and_overrides_are_bounded() {
|
||||
let ok = with(json!({ "preset": "mine", "themes": [{ "id": "mine", "name": "Mine", "colors": null }], "overrides": { "font": { "scale": 1.1 } } })).unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue