#![forbid(unsafe_code)]
pub mod form;
pub mod list;
use makeover_geometry::{Density, SizeClass};
pub use makeover_geometry::{CSS_LAYER, in_css_layer};
use makeover_layout::{Bevel, Depth, Fill, Intent, RowPart, Selector, State, Token, Tone};
use makeover_touch::Affordance;
use std::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Emit {
pub border_width: &'static str,
pub class_prefix: &'static str,
}
impl Default for Emit {
fn default() -> Self {
Self {
border_width: "1px",
class_prefix: "",
}
}
}
#[must_use]
pub fn bevel_var(bevel: Bevel) -> &'static str {
match bevel {
Bevel::Raised => "--bevel-raised",
Bevel::Inset => "--bevel-inset",
}
}
#[must_use]
pub fn fill_var(fill: Fill) -> String {
match fill {
Fill::Well => format!("var(--{}, var(--{}))", fill.token(), Fill::Page.token()),
other => format!("var(--{})", other.token()),
}
}
#[must_use]
pub fn bevel_shadow(bevel: Bevel, opts: &Emit) -> String {
let (top_left, bottom_right) = bevel.edges();
let w = opts.border_width;
format!(
"inset {w} {w} 0 var(--{}), inset -{w} -{w} 0 var(--{})",
top_left.token(),
bottom_right.token()
)
}
#[must_use]
pub fn bevel_properties(opts: &Emit) -> String {
let mut css = String::new();
for bevel in [Bevel::Raised, Bevel::Inset] {
let _ = writeln!(
css,
" {}: {};",
bevel_var(bevel),
bevel_shadow(bevel, opts)
);
}
css
}
#[must_use]
pub fn depth_class(depth: Depth, opts: &Emit) -> Option<String> {
let name = match depth {
Depth::Flat => return None,
Depth::Raised => "raised",
Depth::Well => "well",
Depth::Sunken => "sunken",
_ => return None,
};
Some(format!("{}{name}", opts.class_prefix))
}
fn class(name: &str, opts: &Emit) -> String {
format!("{}{name}", opts.class_prefix)
}
#[must_use]
pub fn depth_declarations(depth: Depth) -> String {
let mut css = String::new();
if let Some(fill) = depth.fill() {
let _ = writeln!(css, " background: {};", fill_var(fill));
}
if let Some(bevel) = depth.bevel() {
let _ = writeln!(css, " box-shadow: var({});", bevel_var(bevel));
}
css
}
#[must_use]
pub fn depth_rule(selector: &str, depth: Depth) -> String {
let body = depth_declarations(depth);
if body.is_empty() {
return String::new();
}
format!(".{selector} {{\n{body}}}\n")
}
fn hover_condition() -> Option<&'static str> {
if Affordance::Hover.available(Density::Touch, SizeClass::Compact) {
None
} else {
Some(Density::Pointer.media_condition())
}
}
fn gated(condition: Option<&str>, rule: &str) -> String {
let Some(condition) = condition else {
return rule.to_string();
};
let mut css = format!("@media {condition} {{\n");
for line in rule.lines() {
if line.is_empty() {
css.push('\n');
} else {
let _ = writeln!(css, " {line}");
}
}
css.push_str("}\n");
css
}
#[must_use]
pub fn focus_rule(selector: &str, depth: Depth, opts: &Emit) -> String {
let w = opts.border_width;
let offset = match depth.bevel() {
Some(Bevel::Inset) => format!("calc(-2 * {w})"),
_ => w.to_string(),
};
format!(
".{selector}:focus-visible {{\n outline: {w} solid var(--{});\n outline-offset: {offset};\n}}\n",
State::Focus.token()
)
}
#[must_use]
pub fn disabled_rule(selector: &str, depth: Depth) -> String {
format!(
".{selector}:disabled,\n.{selector}[aria-disabled=\"true\"] {{\n{} color: var(--{});\n cursor: not-allowed;\n}}\n",
depth_declarations(depth),
State::Disabled.token()
)
}
#[must_use]
pub fn interactive_rules(selector: &str, depth: Depth, opts: &Emit) -> String {
let mut css = gated(
hover_condition(),
&format!(".{selector}:hover {{\n background: var(--hover-surface);\n}}\n"),
);
css.push_str(&depth_rule(
&format!("{selector}:active"),
Depth::Raised.pressed(),
));
css.push_str(&focus_rule(selector, depth, opts));
css.push_str(&disabled_rule(selector, depth));
css
}
#[must_use]
pub fn depth_rules(opts: &Emit) -> String {
let mut css = String::new();
for depth in [Depth::Raised, Depth::Well] {
let Some(class) = depth_class(depth, opts) else {
continue;
};
css.push_str(&depth_rule(&class, depth));
}
if let Some(raised) = depth_class(Depth::Raised, opts) {
css.push_str(&interactive_rules(&raised, Depth::Raised, opts));
}
css
}
fn surface_rules(opts: &Emit) -> String {
let mut css = String::new();
for name in ["button", "card"] {
let c = class(name, opts);
css.push_str(&depth_rule(&c, Depth::Raised));
css.push_str(&interactive_rules(&c, Depth::Raised, opts));
}
let field = class("field", opts);
css.push_str(&depth_rule(&field, Depth::Well));
css.push_str(&focus_rule(&field, Depth::Well, opts));
css.push_str(&disabled_rule(&field, Depth::Well));
let _ = writeln!(
css,
".{field}[aria-invalid=\"true\"] {{\n box-shadow: var({}), 0 0 0 {} var(--danger);\n}}",
bevel_var(Bevel::Inset),
opts.border_width
);
css
}
fn token_rules(opts: &Emit) -> String {
let mut css = String::new();
let badge = class("badge", opts);
let _ = writeln!(
css,
".{badge} {{\n color: var(--{});\n}}",
Tone::Neutral.token()
);
for tone in [Tone::Info, Tone::Success, Tone::Warning, Tone::Danger] {
let _ = writeln!(
css,
".{badge}[data-tone=\"{0}\"] {{\n color: var(--{0});\n}}",
tone.token()
);
}
let chip = class("chip", opts);
let unlatched = Token::Chip { removable: false };
css.push_str(&depth_rule(&chip, unlatched.depth(false)));
css.push_str(&interactive_rules(&chip, unlatched.depth(false), opts));
css.push_str(&depth_rule(
&format!("{chip}.latched"),
unlatched.depth(true),
));
css
}
fn selector_rules(opts: &Emit) -> String {
let mut css = String::new();
for (selector, name) in [
(Selector::Tabs, "tab"),
(Selector::Segmented, "segment"),
(Selector::Toggle, "toggle"),
] {
let c = class(name, opts);
css.push_str(&depth_rule(&c, selector.unchosen()));
css.push_str(&interactive_rules(&c, selector.unchosen(), opts));
css.push_str(&depth_rule(&format!("{c}.chosen"), selector.chosen()));
}
css
}
fn row_rules(opts: &Emit) -> String {
let mut css = String::new();
let row = class("row", opts);
for part in [
RowPart::Primary,
RowPart::Secondary,
RowPart::Meta,
RowPart::Actions,
] {
let name = match part {
RowPart::Primary => "row-primary",
RowPart::Secondary => "row-secondary",
RowPart::Meta => "row-meta",
RowPart::Actions => "row-actions",
};
let c = class(name, opts);
if !matches!(part, RowPart::Actions) {
let _ = writeln!(css, ".{c} {{\n color: var(--{});\n}}", part.intent());
}
if part.revealed_on_hover() {
let _ = writeln!(
css,
".{c} {{\n opacity: 0;\n pointer-events: none;\n}}"
);
let revealed = " opacity: 1;\n pointer-events: auto;\n";
css.push_str(&gated(
hover_condition(),
&format!(".{row}:hover .{c} {{\n{revealed}}}\n"),
));
let _ = write!(css, ".{row}:focus-within .{c} {{\n{revealed}}}\n");
}
}
css
}
fn progress_rules(opts: &Emit) -> String {
let progress = class("progress", opts);
let fill = class("progress-fill", opts);
let mut css = depth_rule(&progress, Depth::Well);
let _ = writeln!(
css,
".{progress} > .{fill} {{\n background: var(--action);\n}}"
);
for tone in [Tone::Info, Tone::Success, Tone::Warning, Tone::Danger] {
let _ = writeln!(
css,
".{progress} > .{fill}[data-tone=\"{0}\"] {{\n background: var(--{0});\n}}",
tone.token()
);
}
css
}
#[must_use]
pub fn component_rules(opts: &Emit) -> String {
let mut css = String::new();
css.push_str(&surface_rules(opts));
css.push_str(&token_rules(opts));
css.push_str(&selector_rules(opts));
css.push_str(&row_rules(opts));
css.push_str(&progress_rules(opts));
css
}
#[must_use]
pub fn stylesheet(opts: &Emit) -> String {
format!(
"/* Generated by makeover-webview from makeover-layout. Do not edit.\n \
Depth is a fill and an edge together; naming them apart is what let\n \
them disagree. See the crate's README and wiki note makeover-layout.\n\n \
Everything below is in the `{CSS_LAYER}` cascade layer. Declare the\n \
order once in your own stylesheet, or this layer's position is decided\n \
by whichever generated file the browser happens to see first:\n\n \
@layer {CSS_LAYER}, base, components, responsive; */\n{}",
in_css_layer(&format!(
":root {{\n{}}}\n\n{}\n{}",
bevel_properties(opts),
depth_rules(opts),
component_rules(opts)
))
)
}
#[cfg(test)]
mod tests {
use super::*;
use makeover_layout::Edge;
#[test]
fn the_emitted_bevel_matches_what_the_apps_already_hand_write() {
let opts = Emit::default();
assert_eq!(
bevel_shadow(Bevel::Raised, &opts),
"inset 1px 1px 0 var(--bevel-light), inset -1px -1px 0 var(--bevel-dark)"
);
assert_eq!(
bevel_shadow(Bevel::Inset, &opts),
"inset 1px 1px 0 var(--bevel-dark), inset -1px -1px 0 var(--bevel-light)"
);
}
#[test]
fn no_colour_ever_reaches_the_output() {
let css = stylesheet(&Emit::default());
assert!(!css.contains('#'), "a hex literal escaped into the CSS");
assert!(
!css.contains("rgb"),
"a colour function escaped into the CSS"
);
assert!(css.contains("var(--surface-raised)"));
assert!(css.contains("var(--bevel-light)"));
}
#[test]
fn a_well_falls_back_through_css_rather_than_through_rust() {
assert_eq!(
fill_var(Fill::Well),
"var(--surface-well, var(--surface-page))"
);
assert_eq!(fill_var(Fill::Raised), "var(--surface-raised)");
assert_eq!(fill_var(Fill::Page), "var(--surface-page)");
}
#[test]
fn raised_and_well_do_not_collapse_onto_each_other() {
let css = depth_rules(&Emit::default());
assert!(css.contains(".raised {"));
assert!(css.contains(".well {"));
assert!(css.contains("var(--bevel-raised)"));
assert!(css.contains("var(--bevel-inset)"));
}
#[test]
fn the_cascade_carries_the_pressed_state() {
let css = depth_rules(&Emit::default());
assert!(css.contains(".raised:active {"));
}
#[test]
fn pressing_moves_the_fill_and_not_only_the_edge() {
let pressed = interactive_rules("button", Depth::Raised, &Emit::default());
assert!(pressed.contains(".button:active {"));
assert!(
pressed.contains("background: var(--surface-well, var(--surface-page))"),
"pressed dropped its fill: {pressed}"
);
assert!(pressed.contains("box-shadow: var(--bevel-inset)"));
}
#[test]
fn pressed_takes_its_fill_from_the_description_not_from_the_app() {
let css = stylesheet(&Emit::default());
let mut checked = 0;
for rule in css.split("}\n") {
if !rule.contains(":active") {
continue;
}
checked += 1;
assert!(
!rule.contains("surface-sunken"),
"a pressed rule took the app's fill: {rule}"
);
}
assert!(checked > 0, "no pressed rules found to check");
assert_eq!(
Depth::Raised.pressed().fill(),
Some(Fill::Well),
"the description changed under us"
);
}
#[test]
fn the_whole_stylesheet_is_emitted_in_the_family_layer() {
let css = stylesheet(&Emit::default());
assert!(css.contains(&format!("@layer {CSS_LAYER} {{")));
assert_eq!(css.matches("@layer").count(), 2, "banner names it once");
let opened = css.find("@layer makeover {").expect("layer opens");
for (i, line) in css.lines().enumerate() {
let before_layer = css.lines().take(i).map(str::len).sum::<usize>() < opened;
if before_layer || line.is_empty() {
continue;
}
assert!(
line.starts_with(" ") || line == "}" || line.starts_with(" "),
"line outside the layer: {line:?}"
);
}
}
#[test]
fn the_generated_sheet_carries_no_trailing_whitespace() {
let css = stylesheet(&Emit::default());
for (i, line) in css.lines().enumerate() {
assert_eq!(line, line.trim_end(), "trailing whitespace on line {i}");
}
}
#[test]
fn the_banner_tells_an_app_how_to_order_the_layer() {
let css = stylesheet(&Emit::default());
assert!(css.contains("@layer makeover, base, components, responsive;"));
assert!(css.starts_with("/* Generated by makeover-webview"));
}
#[test]
fn a_primitive_owns_every_state_it_implies() {
let css = stylesheet(&Emit::default());
for selector in ["button", "card", "chip", "tab", "segment", "toggle"] {
assert!(css.contains(&format!(".{selector}:hover {{")), "{selector}");
assert!(css.contains(&format!(".{selector}:active {{")), "{selector}");
assert!(
css.contains(&format!(".{selector}:focus-visible {{")),
"{selector} has no focus ring"
);
assert!(
css.contains(&format!(".{selector}:disabled,")),
"{selector} has no disabled state"
);
}
}
#[test]
fn a_field_takes_focus_and_refuses_input_without_taking_a_hover() {
let css = stylesheet(&Emit::default());
assert!(css.contains(".field:focus-visible {"));
assert!(css.contains(".field:disabled,"));
assert!(!css.contains(".field:hover {"));
assert!(!css.contains(".field:active {"));
}
#[test]
fn disabled_is_emitted_after_hover_so_source_order_settles_it() {
let css = interactive_rules("button", Depth::Raised, &Emit::default());
let hover = css.find(":hover").expect("hover");
let active = css.find(":active").expect("active");
let focus = css.find(":focus-visible").expect("focus");
let disabled = css.find(":disabled").expect("disabled");
assert!(hover < active && active < focus && focus < disabled);
let tail = &css[disabled..];
assert!(tail.contains("background: var(--surface-raised)"));
}
#[test]
fn a_disabled_state_reaches_things_that_cannot_be_disabled() {
let css = disabled_rule("chip", Depth::Raised);
assert!(css.contains(".chip:disabled,"));
assert!(css.contains(".chip[aria-disabled=\"true\"]"));
assert!(css.contains("cursor: not-allowed"));
}
#[test]
fn the_focus_ring_does_not_disturb_the_bevel_it_lands_on() {
let opts = Emit::default();
let css = focus_rule("button", Depth::Raised, &opts);
assert!(css.contains("outline: 1px solid var(--focus-ring)"));
assert!(!css.contains("box-shadow"), "the ring restated the bevel");
}
#[test]
fn a_well_takes_the_ring_inside_and_a_raised_surface_outside() {
let opts = Emit::default();
assert!(focus_rule("field", Depth::Well, &opts).contains("outline-offset: calc(-2 * 1px)"));
assert!(focus_rule("button", Depth::Raised, &opts).contains("outline-offset: 1px"));
assert!(focus_rule("badge", Depth::Sunken, &opts).contains("outline-offset: 1px"));
}
#[test]
fn hover_is_gated_on_capability_and_the_keyboard_path_is_not() {
let css = stylesheet(&Emit::default());
let condition = format!("@media {}", Density::Pointer.media_condition());
assert!(css.contains(&condition));
let indent = |needle: &str| {
let line = css
.lines()
.find(|l| l.contains(needle))
.unwrap_or_else(|| panic!("no line for {needle}"));
line.len() - line.trim_start().len()
};
let hover = indent(".row:hover .row-actions");
let keyboard = indent(".row:focus-within .row-actions");
assert!(
hover > keyboard,
"hover reveal must be nested inside the capability query and the \
keyboard reveal must not be: hover indent {hover}, keyboard {keyboard}"
);
}
#[test]
fn the_capability_answer_is_asked_for_and_not_assumed() {
assert!(!Affordance::Hover.available(Density::Touch, SizeClass::Compact));
assert!(Affordance::Hover.available(Density::Pointer, SizeClass::Compact));
assert_eq!(hover_condition(), Some(Density::Pointer.media_condition()));
assert!(Affordance::Hover.reads_density());
for size in [SizeClass::Compact, SizeClass::Medium, SizeClass::Expanded] {
assert!(!Affordance::Hover.available(Density::Touch, size));
}
}
#[test]
fn hover_resolves_against_the_token_makeover_already_derives() {
let css = interactive_rules("card", Depth::Raised, &Emit::default());
assert!(css.contains(".card:hover {"));
assert!(css.contains("background: var(--hover-surface)"));
assert!(!css.contains("surface-overlay"));
}
#[test]
fn a_badge_gets_no_edge_and_no_fill() {
let css = token_rules(&Emit::default());
let badge = css
.lines()
.skip_while(|l| !l.starts_with(".badge {"))
.take_while(|l| !l.starts_with('}'))
.collect::<Vec<_>>()
.join("\n");
assert!(!badge.contains("box-shadow"), "badge kept an edge: {badge}");
assert!(!badge.contains("background"), "badge kept a fill: {badge}");
assert_eq!(Token::Badge.depth(false), Depth::Flat);
assert_eq!(Token::Badge.depth(true), Depth::Flat);
}
#[test]
fn a_badge_carries_a_tone_and_neutral_is_the_bare_class() {
let css = token_rules(&Emit::default());
assert!(css.contains(".badge {\n color: var(--content-muted);"));
assert!(!css.contains("data-tone=\"content-muted\""));
for tone in ["info", "success", "warning", "danger"] {
assert!(
css.contains(&format!(".badge[data-tone=\"{tone}\"]")),
"missing tone {tone}"
);
assert!(css.contains(&format!("color: var(--{tone})")));
}
}
#[test]
fn a_chip_is_raised_and_latches_into_a_well() {
let css = token_rules(&Emit::default());
assert!(css.contains(".chip {"));
assert!(css.contains(".chip.latched {"));
assert!(css.contains(".chip:active {"));
assert!(Token::Chip { removable: false }.interactive());
assert!(!Token::Badge.interactive());
}
#[test]
fn only_a_tab_comes_forward_when_chosen() {
let css = selector_rules(&Emit::default());
assert!(css.contains(".tab.chosen {"));
assert!(css.contains(".segment.chosen {"));
assert!(css.contains(".toggle.chosen {"));
assert_eq!(Selector::Tabs.chosen(), Depth::Raised);
assert_eq!(Selector::Segmented.chosen(), Depth::Well);
assert_eq!(Selector::Toggle.chosen(), Depth::Well);
let tab = css
.lines()
.skip_while(|l| !l.starts_with(".tab.chosen {"))
.take_while(|l| !l.starts_with('}'))
.collect::<Vec<_>>()
.join("\n");
assert!(
tab.contains("var(--bevel-raised)"),
"tab was held in: {tab}"
);
}
#[test]
fn an_unchosen_tab_recedes_without_looking_picked() {
let css = selector_rules(&Emit::default());
assert!(
css.contains(".tab {\n background: var(--surface-sunken);\n}"),
"unchosen tab is not recessed: {css}"
);
assert_eq!(Selector::Tabs.unchosen(), Depth::Sunken);
assert!(css.contains(".tab:hover {"));
}
#[test]
fn a_segment_stands_up_so_the_chosen_one_can_be_held_in() {
let css = selector_rules(&Emit::default());
assert!(css.contains(".segment {\n background: var(--surface-raised);"));
assert_eq!(Selector::Segmented.unchosen(), Depth::Raised);
assert_eq!(Selector::Segmented.chosen(), Depth::Well);
}
#[test]
fn row_actions_are_revealed_without_moving_the_row() {
let css = row_rules(&Emit::default());
assert!(css.contains(".row-actions {\n opacity: 0;"));
assert!(!css.contains("display: none"));
assert!(css.contains(".row:focus-within .row-actions"));
assert!(RowPart::Actions.revealed_on_hover());
}
#[test]
fn a_hidden_row_action_is_still_focusable_and_not_tappable() {
let css = row_rules(&Emit::default());
assert!(!css.contains("visibility:"));
assert!(css.contains(".row-actions {\n opacity: 0;\n pointer-events: none;\n}"));
assert!(css.contains("opacity: 1;\n pointer-events: auto;"));
}
#[test]
fn the_three_text_parts_take_their_intents_and_actions_inherits() {
let css = row_rules(&Emit::default());
assert!(css.contains(".row-primary {\n color: var(--content);"));
assert!(css.contains(".row-secondary {\n color: var(--content-secondary);"));
assert!(css.contains(".row-meta {\n color: var(--content-muted);"));
assert!(!css.contains(".row-actions {\n color:"));
}
#[test]
fn the_progress_trough_is_a_well() {
let css = progress_rules(&Emit::default());
assert!(css.contains(".progress {"));
assert!(css.contains("box-shadow: var(--bevel-inset)"));
assert!(css.contains(".progress > .progress-fill {"));
assert!(css.contains("background: var(--action)"));
assert!(!css.contains("> .fill "));
}
#[test]
fn a_progress_bar_can_carry_a_tone_and_defaults_to_action() {
let css = progress_rules(&Emit::default());
assert!(css.contains(".progress > .progress-fill {\n background: var(--action);"));
assert!(!css.contains("progress-fill {\n color: var(--content-muted)"));
for tone in ["info", "success", "warning", "danger"] {
assert!(
css.contains(&format!(".progress > .progress-fill[data-tone=\"{tone}\"]")),
"missing progress tone {tone}"
);
}
assert!(css.contains("[data-tone=\"success\"] {\n background: var(--success);"));
assert!(css.contains("[data-tone=\"danger\"] {\n background: var(--danger);"));
}
#[test]
fn no_scrollbar_track_is_emitted() {
let css = stylesheet(&Emit::default());
assert!(!css.contains("scrollbar"));
assert!(!css.contains("::-webkit"));
}
#[test]
fn an_invalid_field_is_ringed_without_being_lit() {
let css = surface_rules(&Emit::default());
assert!(css.contains(".field {"));
assert!(css.contains(".field[aria-invalid=\"true\"] {"));
assert!(!css.contains(".field.invalid"));
assert!(css.contains("0 0 0 1px var(--danger)"));
}
#[test]
fn an_invalid_field_keeps_the_well_underneath_it() {
let css = surface_rules(&Emit::default());
let invalid = css
.lines()
.skip_while(|l| !l.starts_with(".field[aria-invalid"))
.take_while(|l| !l.starts_with('}'))
.collect::<Vec<_>>()
.join("\n");
assert!(
invalid.contains("var(--bevel-inset)"),
"the well was dropped: {invalid}"
);
assert!(invalid.contains("var(--danger)"));
}
#[test]
fn button_and_card_come_out_identical_by_construction() {
let opts = Emit::default();
let css = surface_rules(&opts);
assert_eq!(
depth_declarations(Depth::Raised),
depth_declarations(Depth::Raised)
);
assert!(css.contains(".button {"));
assert!(css.contains(".card {"));
assert_eq!(
interactive_rules("button", Depth::Raised, &Emit::default()).replace("button", "card"),
interactive_rules("card", Depth::Raised, &Emit::default())
);
}
#[test]
fn a_prefix_reaches_the_component_classes_too() {
let opts = Emit {
class_prefix: "mo-",
..Emit::default()
};
let css = stylesheet(&opts);
for name in [
"mo-button",
"mo-card",
"mo-field",
"mo-badge",
"mo-chip",
"mo-tab",
"mo-row-primary",
"mo-progress",
"mo-progress-fill",
] {
assert!(css.contains(&format!(".{name}")), "unprefixed: {name}");
}
assert!(!css.contains(".button {"));
assert!(!css.contains(".card {"));
assert!(!css.contains(".badge {"));
}
#[test]
fn the_whole_sheet_still_names_every_colour() {
let css = stylesheet(&Emit::default());
assert!(!css.contains('#'));
assert!(!css.contains("rgb"));
for line in css.lines() {
let trimmed = line.trim();
if !trimmed.ends_with(';') {
continue;
}
let Some((_, value)) = trimmed.split_once(": ") else {
continue;
};
if value.contains("var(--") {
continue;
}
let width = Emit::default().border_width;
assert!(
value.contains("inset")
|| value.contains(width)
|| matches!(
value.trim_end_matches(';'),
"0" | "1" | "none" | "auto" | "not-allowed"
),
"unrecognised literal value: {line}"
);
}
}
#[test]
fn flat_emits_nothing_at_all() {
assert_eq!(depth_class(Depth::Flat, &Emit::default()), None);
assert!(!depth_rules(&Emit::default()).contains("flat"));
}
#[test]
fn a_prefix_namespaces_every_class() {
let opts = Emit {
class_prefix: "mo-",
..Emit::default()
};
let css = depth_rules(&opts);
assert!(css.contains(".mo-raised {"));
assert!(css.contains(".mo-well {"));
assert!(!css.contains(".raised {"));
}
#[test]
fn the_border_width_is_the_callers() {
let opts = Emit {
border_width: "2px",
..Emit::default()
};
assert!(bevel_shadow(Bevel::Raised, &opts).contains("inset 2px 2px 0"));
}
#[test]
fn edges_agree_with_the_description() {
let (tl, br) = Bevel::Raised.edges();
assert_eq!(tl.token(), Edge::Light.token());
assert_eq!(br.token(), Edge::Dark.token());
}
}