pub(crate) const FOCUS_RING: &str =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2";
pub(crate) const MOTION_FAST: &str = "transition-colors duration-fast ease-base";
pub(crate) const MOTION_BASE: &str = "transition-opacity duration-base ease-base";
pub(crate) const DISABLED_BASE: &str = "disabled:opacity-50 disabled:pointer-events-none";
pub(crate) const INTERACTIVE_BASE: &str = concat!(
"transition-colors duration-fast ease-base ",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
);
pub const TOUCH_ACTION: &str = "touch-manipulation";
pub const HIT_TARGET_MIN: &str = "min-h-[44px] min-w-[44px]";
pub const HIT_TARGET_NUMPAD: &str = "min-h-[56px] min-w-[56px]";
pub const PRESS_ACTIVE: &str = "active:scale-95 active:bg-border";
pub const OVERSCROLL_CONTAIN: &str = "overscroll-contain";
pub const TAP_HIGHLIGHT: &str = "pos-tap-highlight";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interactive_base_is_motion_fast_plus_focus_ring() {
assert_eq!(INTERACTIVE_BASE, format!("{MOTION_FAST} {FOCUS_RING}"));
}
#[test]
fn fragments_use_token_utilities() {
assert!(FOCUS_RING.contains("focus-visible:ring-ring"));
assert!(MOTION_FAST.contains("duration-fast") && MOTION_FAST.contains("ease-base"));
assert!(MOTION_BASE.contains("duration-base") && MOTION_BASE.contains("ease-base"));
assert!(DISABLED_BASE.contains("disabled:pointer-events-none"));
}
#[test]
fn render_functions_use_constants_not_literals() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let render_dir = std::path::Path::new(&manifest_dir).join("src/render");
let guarded = [
"touch-manipulation",
"min-h-[44px] min-w-[44px]",
"min-h-[56px] min-w-[56px]",
];
for entry in std::fs::read_dir(&render_dir).expect("src/render readable") {
let path = entry.unwrap().path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
if filename == "classes.rs" {
continue;
}
let source = std::fs::read_to_string(&path).unwrap();
for lit in &guarded {
assert!(
!source.contains(lit),
"{filename}: raw POS literal {lit:?} — import from render::classes instead"
);
}
}
}
#[test]
fn atoms_render_fns_contain_no_migrated_appearance_utilities() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let atoms_path = std::path::Path::new(&manifest_dir).join("src/render/atoms.rs");
let source = std::fs::read_to_string(&atoms_path).expect("atoms.rs readable");
let test_start = source.find("#[cfg(test)]").unwrap_or(source.len());
let production_source = &source[..test_start];
let banned: &[(&str, &str)] = &[
(
"bg-primary/70",
"toast tone bg — use fjui-toast--neutral skin rule",
),
(
"bg-success/70",
"toast tone bg — use fjui-toast--success skin rule",
),
(
"bg-warning/70",
"toast tone bg — use fjui-toast--warning skin rule",
),
(
"bg-destructive/70",
"toast tone bg — use fjui-toast--destructive skin rule",
),
(
"fjui-action-card\" class=\"",
"action card must not carry appearance utilities after fjui-action-card",
),
(
"bg-primary text-primary-foreground\">",
"pagination active page — use fjui-pagination__btn--active",
),
];
for (lit, reason) in banned {
assert!(
!production_source.contains(lit),
"atoms.rs: appearance utility {lit:?} found — {reason}"
);
}
}
#[test]
fn containers_render_fns_contain_no_migrated_appearance_utilities() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let containers_path = std::path::Path::new(&manifest_dir).join("src/render/containers.rs");
let source = std::fs::read_to_string(&containers_path).expect("containers.rs readable");
let test_start = source.find("#[cfg(test)]").unwrap_or(source.len());
let production_source = &source[..test_start];
let banned: &[(&str, &str)] = &[
(
"rounded-lg border border-border bg-card",
"card outer — use fjui-card skin rule",
),
(
"shadow-sm rounded-lg",
"card bordered shadow — use fjui-card skin rule",
),
(
"shadow-md rounded-lg",
"card elevated shadow — use fjui-card skin rule",
),
(
"<div class=\"border-b border-border\">",
"tabs wrapper div — use fjui-tabs nav (no wrapper div needed)",
),
(
"border-b-2 border-primary",
"active tab underline — use fjui-tab--active skin rule",
),
(
"text-text-muted hover:text-text",
"tab inactive text — use fjui-tab skin rule",
),
(
"bg-card rounded-lg shadow-lg",
"modal dialog — use fjui-modal skin rule",
),
(
"bg-secondary/10 rounded-lg",
"kanban column bg — use fjui-kanban__column skin rule",
),
(
"bg-primary text-primary-foreground",
"kanban active count badge — use fjui-badge--neutral skin rule",
),
(
"border-b border-border bg-background",
"page header bar — use fjui-page-header skin rule",
),
];
for (lit, reason) in banned {
assert!(
!production_source.contains(lit),
"containers.rs: appearance utility {lit:?} found — {reason}"
);
}
}
#[test]
fn data_render_fns_contain_no_migrated_appearance_utilities() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let data_path = std::path::Path::new(&manifest_dir).join("src/render/data.rs");
let source = std::fs::read_to_string(&data_path).expect("data.rs readable");
let test_start = source.find("#[cfg(test)]").unwrap_or(source.len());
let production_source = &source[..test_start];
let banned: &[(&str, &str)] = &[
(
"even:bg-surface",
"DataTable row zebra — removed by RSK-01; skin hover replaces it",
),
(
"odd:bg-surface",
"DataTable row zebra — removed by RSK-01; skin hover replaces it",
),
];
for (lit, reason) in banned {
assert!(
!production_source.contains(lit),
"data.rs: appearance utility {lit:?} found — {reason}"
);
}
}
#[test]
fn form_render_fns_contain_no_migrated_appearance_utilities() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let form_path = std::path::Path::new(&manifest_dir).join("src/render/form.rs");
let source = std::fs::read_to_string(&form_path).expect("form.rs readable");
let test_start = source.find("#[cfg(test)]").unwrap_or(source.len());
let production_source = &source[..test_start];
let banned: &[(&str, &str)] = &[
(
"rounded-md border border-border px-3 py-2",
"Input/Textarea border+padding — use fjui-input skin rule",
),
(
"rounded-md border border-destructive px-3 py-2",
"Input error border — use fjui-input--error skin rule",
),
(
"appearance-none bg-background rounded-md border",
"Select appearance — use fjui-select skin rule",
),
];
for (lit, reason) in banned {
assert!(
!production_source.contains(lit),
"form.rs: appearance utility {lit:?} found — {reason}"
);
}
}
#[test]
fn touch_constants_are_full_literals_and_token_compliant() {
assert_eq!(TOUCH_ACTION, "touch-manipulation");
assert_eq!(HIT_TARGET_MIN, "min-h-[44px] min-w-[44px]");
assert_eq!(HIT_TARGET_NUMPAD, "min-h-[56px] min-w-[56px]");
assert_eq!(OVERSCROLL_CONTAIN, "overscroll-contain");
assert_eq!(TAP_HIGHLIGHT, "pos-tap-highlight");
assert!(PRESS_ACTIVE.contains("active:scale-95"));
assert!(PRESS_ACTIVE.contains("active:bg-border"));
for raw in ["red-", "blue-", "orange-", "zinc-", "gray-", "slate-"] {
assert!(
!PRESS_ACTIVE.contains(raw),
"raw palette class in PRESS_ACTIVE"
);
}
}
}