Menu
Public repository / master
AkurAI-Framework / crates/css/src/utilities/effects.rs
Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson
//! Effects: box shadows and rings (composed like Tailwind through
//! `--tw-shadow` / `--tw-ring-*` so `shadow-md ring-2` show both), opacity,
//! blur/backdrop-blur and transforms (`translate-*`, `scale-*`, `rotate-*`).
//!
//! The composed custom properties are registered with `@property … inherits:
//! false` (emitted once at the end of the stylesheet) so a parent's shadow or
//! ring never leaks into its children; every `var()` read has a fallback, so
//! no preflight is needed.
use super::value::{arbitrary, integer, length_or_fraction, negate, trim_float};
use super::{register, Utility};
/// Registrations for every property in the shadow/ring composition.
const BOX_SHADOW_PROPS: &[&str] = &[
register!("--tw-shadow"),
register!("--tw-ring-shadow"),
register!("--tw-ring-offset-shadow"),
register!("--tw-ring-color"),
register!("--tw-ring-offset-color"),
register!("--tw-ring-offset-width"),
register!("--tw-ring-inset"),
];
const TRANSLATE_PROPS: &[&str] = &[register!("--tw-translate-x"), register!("--tw-translate-y")];
const SHADOWS: &[(&str, &str)] = &[
("sm", "0 1px 2px 0 rgba(0,0,0,0.05)"),
(
"",
"0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px -1px rgba(0,0,0,0.1)",
),
(
"md",
"0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -2px rgba(0,0,0,0.1)",
),
(
"lg",
"0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -4px rgba(0,0,0,0.1)",
),
(
"xl",
"0 20px 25px -5px rgba(0,0,0,0.1),0 8px 10px -6px rgba(0,0,0,0.1)",
),
("2xl", "0 25px 50px -12px rgba(0,0,0,0.25)"),
("inner", "inset 0 2px 4px 0 rgba(0,0,0,0.05)"),
("none", "0 0 #0000"),
];
/// Default `ring` width (Tailwind v3).
const DEFAULT_RING_PX: u32 = 3;
const BLURS: &[(&str, &str)] = &[
("none", "0"),
("sm", "4px"),
("", "8px"),
("md", "12px"),
("lg", "16px"),
("xl", "24px"),
("2xl", "40px"),
("3xl", "64px"),
];
pub fn resolve(u: &str) -> Option<Utility> {
shadow(u)
.or_else(|| ring(u))
.or_else(|| opacity(u))
.or_else(|| blur(u))
.or_else(|| translate(u, false))
.or_else(|| scale(u))
.or_else(|| rotate(u, false))
.or_else(|| (u == "transform-none").then(transform_none))
}
/// `-translate-x-1/2`, `-rotate-45`.
pub fn negative(u: &str) -> Option<Utility> {
translate(u, true).or_else(|| rotate(u, true))
}
/// Strip `prefix` and return either the `-suffix` or `""` for the bare name
/// (`shadow` ⇒ `""`, `shadow-md` ⇒ `"md"`).
fn suffix<'a>(u: &'a str, prefix: &str) -> Option<&'a str> {
let rest = u.strip_prefix(prefix)?;
if rest.is_empty() {
return Some("");
}
rest.strip_prefix('-').filter(|s| !s.is_empty())
}
fn shadow(u: &str) -> Option<Utility> {
let size = suffix(u, "shadow")?;
let (_, value) = SHADOWS.iter().find(|(k, _)| *k == size)?;
Some(
Utility::from_decls(vec![
format!("--tw-shadow:{value}"),
"box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),\
var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)"
.to_string(),
])
.extras(BOX_SHADOW_PROPS),
)
}
/// `ring`, `ring-N`, `ring-[3px]`, `ring-inset`, `ring-offset-N`.
fn ring(u: &str) -> Option<Utility> {
if u == "ring-inset" {
return Some(Utility::decl("--tw-ring-inset", "inset").extras(BOX_SHADOW_PROPS));
}
if let Some(v) = u.strip_prefix("ring-offset-") {
let width = px_or_arbitrary(v)?;
return Some(Utility::decl("--tw-ring-offset-width", &width).extras(BOX_SHADOW_PROPS));
}
let width = match suffix(u, "ring")? {
"" => format!("{DEFAULT_RING_PX}px"),
v => px_or_arbitrary(v)?,
};
Some(
Utility::from_decls(vec![
"--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 \
var(--tw-ring-offset-width,0px) var(--tw-ring-offset-color,var(--bg))"
.to_string(),
format!(
"--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 \
calc({width} + var(--tw-ring-offset-width,0px)) \
var(--tw-ring-color,var(--accent))"
),
"box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),\
var(--tw-shadow,0 0 #0000)"
.to_string(),
])
.extras(BOX_SHADOW_PROPS),
)
}
/// `N` ⇒ `Npx`, or an arbitrary length.
fn px_or_arbitrary(v: &str) -> Option<String> {
integer(v).map(|n| format!("{n}px")).or_else(|| {
let a = arbitrary(v)?;
super::value::looks_like_length(&a).then_some(a)
})
}
/// `opacity-0`…`opacity-100` ⇒ `0`…`1`.
fn opacity(u: &str) -> Option<Utility> {
let v = u.strip_prefix("opacity-")?;
let value = match integer(v) {
Some(n) if n <= 100 => trim_float(f64::from(n) / 100.0),
Some(_) => return None,
None => arbitrary(v)?,
};
Some(Utility::decl("opacity", &value))
}
/// `blur-*` and `backdrop-blur-*`.
fn blur(u: &str) -> Option<Utility> {
let (backdrop, size) = match suffix(u, "backdrop-blur") {
Some(size) => (true, size),
None => (false, suffix(u, "blur")?),
};
let radius = match BLURS.iter().find(|(k, _)| *k == size) {
Some((_, r)) => (*r).to_string(),
None => arbitrary(size)?,
};
let filter = format!("blur({radius})");
Some(if backdrop {
Utility::props(&["-webkit-backdrop-filter", "backdrop-filter"], &filter)
} else {
Utility::decl("filter", &filter)
})
}
/// `translate-x-4` ⇒ `--tw-translate-x:16px;translate:var(--tw-translate-x)
/// var(--tw-translate-y,0)`; the axes compose in any order.
fn translate(u: &str, neg: bool) -> Option<Utility> {
let rest = u.strip_prefix("translate-")?;
let (axis, v) = rest.split_once('-')?;
if axis != "x" && axis != "y" {
return None;
}
let mut value = length_or_fraction(v)?;
if neg {
value = negate(&value)?;
}
let pair = if axis == "x" {
"var(--tw-translate-x) var(--tw-translate-y,0)"
} else {
"var(--tw-translate-x,0) var(--tw-translate-y)"
};
Some(
Utility::from_decls(vec![
format!("--tw-translate-{axis}:{value}"),
format!("translate:{pair}"),
])
.extras(TRANSLATE_PROPS),
)
}
/// `scale-95` ⇒ `scale:95%`.
fn scale(u: &str) -> Option<Utility> {
let v = u.strip_prefix("scale-")?;
let value = match integer(v) {
Some(n) => format!("{n}%"),
None => arbitrary(v)?,
};
Some(Utility::decl("scale", &value))
}
/// `rotate-45` ⇒ `rotate:45deg`.
fn rotate(u: &str, neg: bool) -> Option<Utility> {
let v = u.strip_prefix("rotate-")?;
let value = match integer(v) {
Some(0) => "0deg".to_string(),
Some(n) if neg => format!("-{n}deg"),
Some(n) => format!("{n}deg"),
None if !neg => arbitrary(v)?,
None => return None,
};
Some(Utility::decl("rotate", &value))
}
fn transform_none() -> Utility {
Utility::body("transform:none;translate:none;scale:none;rotate:none")
}
#[cfg(test)]
mod tests {
use super::*;
fn decl(u: &str) -> Option<String> {
resolve(u).map(|x| x.declaration())
}
#[test]
fn shadow_sizes() {
assert!(decl("shadow-2xl").unwrap().contains("rgba(0,0,0,0.25)"));
assert!(decl("shadow-inner")
.unwrap()
.starts_with("--tw-shadow:inset"));
assert_eq!(decl("shadow-huge"), None);
assert_eq!(decl("shadowy"), None);
assert_eq!(decl("shadow-"), None);
}
#[test]
fn ring_parsing() {
assert!(decl("ring-[5px]").unwrap().contains("calc(5px + "));
assert_eq!(decl("ring-[#fff]"), None);
assert_eq!(decl("ring-accent"), None);
assert_eq!(
decl("ring-offset-4").as_deref(),
Some("--tw-ring-offset-width:4px")
);
}
#[test]
fn blur_and_transforms() {
assert_eq!(decl("blur-none").as_deref(), Some("filter:blur(0)"));
assert_eq!(decl("blur-[2px]").as_deref(), Some("filter:blur(2px)"));
assert_eq!(decl("translate-z-4"), None);
assert_eq!(decl("rotate-[1turn]").as_deref(), Some("rotate:1turn"));
assert!(negative("translate-y-[3px]")
.unwrap()
.declaration()
.contains("-3px"));
assert_eq!(
negative("rotate-0").map(|x| x.declaration()).as_deref(),
Some("rotate:0deg")
);
}
}