#![forbid(unsafe_code)]
pub trait Intent {
fn token(self) -> &'static str;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Bevel {
Raised,
Inset,
}
impl Bevel {
#[must_use]
pub const fn edges(self) -> (Edge, Edge) {
match self {
Self::Raised => (Edge::Light, Edge::Dark),
Self::Inset => (Edge::Dark, Edge::Light),
}
}
#[must_use]
pub const fn pressed(self) -> Self {
match self {
Self::Raised => Self::Inset,
Self::Inset => Self::Raised,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Edge {
Light,
Dark,
}
impl Intent for Edge {
fn token(self) -> &'static str {
match self {
Self::Light => "bevel-light",
Self::Dark => "bevel-dark",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Fill {
Page,
Raised,
Overlay,
Well,
Sunken,
}
impl Intent for Fill {
fn token(self) -> &'static str {
match self {
Self::Page => "surface-page",
Self::Raised => "surface-raised",
Self::Overlay => "surface-overlay",
Self::Well => "surface-well",
Self::Sunken => "surface-sunken",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Depth {
Flat,
Raised,
Well,
Sunken,
}
impl Depth {
#[must_use]
pub const fn bevel(self) -> Option<Bevel> {
match self {
Self::Flat | Self::Sunken => None,
Self::Raised => Some(Bevel::Raised),
Self::Well => Some(Bevel::Inset),
}
}
#[must_use]
pub const fn fill(self) -> Option<Fill> {
match self {
Self::Flat => None,
Self::Raised => Some(Fill::Raised),
Self::Well => Some(Fill::Well),
Self::Sunken => Some(Fill::Sunken),
}
}
#[must_use]
pub const fn pressed(self) -> Self {
match self {
Self::Raised => Self::Well,
other => other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum State {
Focus,
Disabled,
}
impl State {
#[must_use]
pub const fn suppresses_interaction(self) -> bool {
match self {
Self::Disabled => true,
Self::Focus => false,
}
}
}
impl Intent for State {
fn token(self) -> &'static str {
match self {
Self::Focus => "focus-ring",
Self::Disabled => "content-muted",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Tone {
Neutral,
Info,
Success,
Warning,
Danger,
}
impl Intent for Tone {
fn token(self) -> &'static str {
match self {
Self::Neutral => "content-muted",
Self::Info => "info",
Self::Success => "success",
Self::Warning => "warning",
Self::Danger => "danger",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Token {
Badge,
Chip {
removable: bool,
},
}
impl Token {
#[must_use]
pub const fn interactive(self) -> bool {
matches!(self, Self::Chip { .. })
}
#[must_use]
pub const fn depth(self, latched: bool) -> Depth {
match self {
Self::Badge => Depth::Flat,
Self::Chip { .. } if latched => Depth::Well,
Self::Chip { .. } => Depth::Raised,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Notice {
Toast,
Banner,
}
impl Notice {
#[must_use]
pub const fn transient(self) -> bool {
matches!(self, Self::Toast)
}
#[must_use]
pub const fn fill(self) -> Fill {
match self {
Self::Toast => Fill::Overlay,
Self::Banner => Fill::Raised,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RowPart {
Primary,
Secondary,
Meta,
Actions,
Tokens,
}
impl RowPart {
#[must_use]
pub const fn revealed_on_hover(self) -> bool {
matches!(self, Self::Actions)
}
#[must_use]
pub const fn intent(self) -> &'static str {
match self {
Self::Primary => "content",
Self::Secondary => "content-secondary",
Self::Meta => "content-muted",
Self::Actions => "content",
Self::Tokens => "content",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Heading {
Page,
Section,
Subsection,
}
impl Heading {
#[must_use]
pub const fn separated(self) -> bool {
matches!(self, Self::Section)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Selector {
Segmented,
Toggle,
Tabs,
}
impl Selector {
#[must_use]
pub const fn chosen(self) -> Depth {
match self {
Self::Segmented | Self::Toggle => Depth::Well,
Self::Tabs => Depth::Raised,
}
}
#[must_use]
pub const fn unchosen(self) -> Depth {
match self {
Self::Tabs => Depth::Sunken,
Self::Segmented | Self::Toggle => Depth::Raised,
}
}
#[must_use]
pub const fn abutting(self) -> bool {
matches!(self, Self::Segmented | Self::Tabs)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Readiness {
Ready,
Pending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Meter<'a> {
pub done: u32,
pub total: u32,
pub tone: Tone,
pub label: Option<&'a str>,
}
impl<'a> Meter<'a> {
#[must_use]
pub const fn new(done: u32, total: u32) -> Self {
Self {
done,
total,
tone: Tone::Neutral,
label: None,
}
}
#[must_use]
pub const fn tone(mut self, tone: Tone) -> Self {
self.tone = tone;
self
}
#[must_use]
pub const fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
#[must_use]
pub const fn percent(&self) -> u8 {
if self.total == 0 {
return 0;
}
let scaled = (self.done as u64 * 100) / self.total as u64;
if scaled > 100 { 100 } else { scaled as u8 }
}
#[must_use]
pub const fn overflowing(&self) -> bool {
self.done > self.total
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.total == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Region<'a> {
Band,
Sidebar,
Pane,
Split,
TabGroup,
Modal,
Bespoke {
name: &'a str,
},
}
impl Region<'_> {
#[must_use]
pub const fn depth(self) -> Depth {
match self {
Self::Band | Self::Sidebar | Self::Split | Self::TabGroup => Depth::Flat,
Self::Pane => Depth::Well,
Self::Modal => Depth::Raised,
Self::Bespoke { .. } => Depth::Flat,
}
}
#[must_use]
pub const fn described(self) -> bool {
!matches!(self, Self::Bespoke { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Arrangement {
ListDetail {
tabbed: bool,
},
SidebarContent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum FieldKind {
Text,
Secret,
Number,
Email,
Url,
Tel,
Textarea,
Select,
Radio,
Checkbox,
Hidden,
}
impl FieldKind {
#[must_use]
pub const fn visible(self) -> bool {
!matches!(self, Self::Hidden)
}
#[must_use]
pub const fn confidential(self) -> bool {
matches!(self, Self::Secret)
}
#[must_use]
pub const fn labels_itself(self) -> bool {
matches!(self, Self::Checkbox)
}
#[must_use]
pub const fn offers_options(self) -> bool {
matches!(self, Self::Select | Self::Radio)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Choice<'a> {
pub value: &'a str,
pub label: &'a str,
}
impl<'a> Choice<'a> {
#[must_use]
pub const fn plain(value: &'a str) -> Self {
Self {
value,
label: value,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Field<'a> {
pub kind: FieldKind,
pub name: &'a str,
pub label: &'a str,
pub hint: Option<&'a str>,
pub error: Option<&'a str>,
pub placeholder: Option<&'a str>,
pub options: &'a [Choice<'a>],
pub required: bool,
pub extended: bool,
}
impl<'a> Field<'a> {
#[must_use]
pub const fn new(kind: FieldKind, name: &'a str, label: &'a str) -> Self {
Self {
kind,
name,
label,
hint: None,
error: None,
placeholder: None,
options: &[],
required: false,
extended: false,
}
}
#[must_use]
pub const fn select(name: &'a str, label: &'a str, options: &'a [Choice<'a>]) -> Self {
Self::offering(FieldKind::Select, name, label, options)
}
#[must_use]
pub const fn radio(name: &'a str, label: &'a str, options: &'a [Choice<'a>]) -> Self {
Self::offering(FieldKind::Radio, name, label, options)
}
const fn offering(
kind: FieldKind,
name: &'a str,
label: &'a str,
options: &'a [Choice<'a>],
) -> Self {
Self {
options,
..Self::new(kind, name, label)
}
}
#[must_use]
pub const fn invalid(&self) -> bool {
self.error.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Width {
Content,
Fixed,
Fill,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Priority {
Optional,
Secondary,
Essential,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Column<'a> {
pub name: &'a str,
pub width: Width,
pub priority: Priority,
}
impl<'a> Column<'a> {
#[must_use]
pub const fn new(name: &'a str) -> Self {
Self {
name,
width: Width::Fill,
priority: Priority::Secondary,
}
}
#[must_use]
pub const fn kept_at(&self, cutoff: Priority) -> bool {
(self.priority as u8) >= (cutoff as u8)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_meter_keeps_the_over_run_the_percentage_throws_away() {
let over = Meter::new(45, 30);
assert_eq!(over.percent(), 100);
assert!(over.overflowing());
let exact = Meter::new(30, 30);
assert_eq!(exact.percent(), over.percent());
assert!(!exact.overflowing());
}
#[test]
fn an_empty_set_does_not_divide_by_zero() {
let none = Meter::new(0, 0);
assert_eq!(none.percent(), 0);
assert!(none.is_empty());
assert!(!none.overflowing());
}
#[test]
fn the_ratio_survives_where_a_percentage_would_not() {
let m = Meter::new(3, 7).label("subtasks");
assert_eq!(m.percent(), 42);
assert_eq!((m.done, m.total), (3, 7));
assert_eq!(m.label, Some("subtasks"));
}
#[test]
fn tone_is_carried_because_no_renderer_can_derive_it() {
let subtasks = Meter::new(9, 10).tone(Tone::Success);
let estimate = Meter::new(9, 10).tone(Tone::Danger);
assert_eq!(subtasks.percent(), estimate.percent());
assert_ne!(subtasks.tone, estimate.tone);
assert_eq!(Meter::new(9, 10).tone, Tone::Neutral);
}
#[test]
fn a_meter_does_not_overflow_on_large_counts() {
let big = Meter::new(u32::MAX, u32::MAX);
assert_eq!(big.percent(), 100);
assert!(!big.overflowing());
}
#[test]
fn inset_is_raised_with_the_light_moved() {
let (rl, rd) = Bevel::Raised.edges();
let (il, id) = Bevel::Inset.edges();
assert_eq!((rl, rd), (Edge::Light, Edge::Dark));
assert_eq!((il, id), (rd, rl));
}
#[test]
fn pressing_twice_is_a_no_op() {
for b in [Bevel::Raised, Bevel::Inset] {
assert_eq!(b.pressed().pressed(), b);
}
}
#[test]
fn a_raised_region_is_never_filled_with_a_recessed_surface() {
assert_eq!(Depth::Raised.fill(), Some(Fill::Raised));
assert_eq!(Depth::Raised.bevel(), Some(Bevel::Raised));
assert_eq!(Depth::Well.bevel(), Some(Bevel::Inset));
assert_ne!(Depth::Well.fill(), Depth::Raised.fill());
}
#[test]
fn state_is_orthogonal_to_depth() {
assert_eq!(Depth::Raised.fill(), Some(Fill::Raised));
assert_eq!(Depth::Well.fill(), Some(Fill::Well));
assert!(State::Disabled.suppresses_interaction());
}
#[test]
fn only_disabled_stops_answering() {
assert!(!State::Focus.suppresses_interaction());
assert!(State::Disabled.suppresses_interaction());
}
#[test]
fn both_states_resolve_against_intents_makeover_already_derives() {
assert_eq!(State::Focus.token(), "focus-ring");
assert_eq!(State::Disabled.token(), "content-muted");
}
#[test]
fn flat_has_neither_edge_nor_fill() {
assert_eq!(Depth::Flat.bevel(), None);
assert_eq!(Depth::Flat.fill(), None);
}
#[test]
fn sunken_is_recessed_by_colour_with_no_edge() {
assert_eq!(Depth::Sunken.fill(), Some(Fill::Sunken));
assert_eq!(Depth::Sunken.bevel(), None);
}
#[test]
fn sunken_and_flat_are_different_claims() {
assert_eq!(Depth::Flat.bevel(), Depth::Sunken.bevel());
assert_ne!(Depth::Flat.fill(), Depth::Sunken.fill());
}
#[test]
fn a_sunken_surface_is_not_a_well() {
assert_ne!(Fill::Sunken, Fill::Well);
assert_eq!(Fill::Sunken.token(), "surface-sunken");
assert_eq!(Fill::Well.token(), "surface-well");
}
#[test]
fn every_selector_describes_both_of_its_states() {
for s in [Selector::Tabs, Selector::Segmented, Selector::Toggle] {
assert_ne!(
s.chosen(),
s.unchosen(),
"{s:?} cannot tell picked from unpicked"
);
}
}
#[test]
fn only_a_tab_inverts_the_other_way() {
assert_eq!(Selector::Tabs.unchosen(), Depth::Sunken);
assert_eq!(Selector::Tabs.chosen(), Depth::Raised);
for s in [Selector::Segmented, Selector::Toggle] {
assert_eq!(s.unchosen(), Depth::Raised);
assert_eq!(s.chosen(), Depth::Well);
assert_eq!(s.unchosen().pressed(), s.chosen());
}
}
#[test]
fn pressing_a_card_makes_a_well() {
assert_eq!(Depth::Raised.pressed(), Depth::Well);
assert_eq!(
Depth::Raised.pressed().bevel(),
Depth::Raised.bevel().map(Bevel::pressed)
);
assert_eq!(Depth::Flat.pressed(), Depth::Flat);
assert_eq!(Depth::Well.pressed(), Depth::Well);
}
#[test]
fn intents_name_makeover_tokens_and_nothing_else() {
assert_eq!(Edge::Light.token(), "bevel-light");
assert_eq!(Edge::Dark.token(), "bevel-dark");
assert_eq!(Fill::Raised.token(), "surface-raised");
assert_eq!(Fill::Well.token(), "surface-well");
for t in [
Edge::Light.token(),
Edge::Dark.token(),
Tone::Danger.token(),
Tone::Neutral.token(),
State::Focus.token(),
State::Disabled.token(),
] {
assert!(!t.starts_with('#'), "{t} looks like a value");
assert!(
!t.chars().next().unwrap().is_ascii_digit(),
"{t} is a value"
);
}
}
#[test]
fn a_badge_cannot_be_pressed_and_a_chip_latches() {
assert!(!Token::Badge.interactive());
assert!(Token::Chip { removable: false }.interactive());
assert!(Token::Chip { removable: true }.interactive());
assert_eq!(Token::Badge.depth(false), Depth::Flat);
assert_eq!(Token::Badge.depth(true), Depth::Flat);
let chip = Token::Chip { removable: false };
assert_eq!(chip.depth(false), Depth::Raised);
assert_eq!(chip.depth(true), Depth::Raised.pressed());
}
#[test]
fn a_toast_and_a_banner_differ_in_more_than_placement() {
assert!(Notice::Toast.transient());
assert!(!Notice::Banner.transient());
assert_eq!(Notice::Toast.fill(), Fill::Overlay);
assert_eq!(Notice::Banner.fill(), Fill::Raised);
}
#[test]
fn only_the_actions_part_hides_until_hovered() {
for p in [
RowPart::Primary,
RowPart::Secondary,
RowPart::Meta,
RowPart::Tokens,
] {
assert!(!p.revealed_on_hover(), "{p:?} should always be visible");
}
assert!(RowPart::Actions.revealed_on_hover());
assert_eq!(RowPart::Primary.intent(), "content");
assert_eq!(RowPart::Secondary.intent(), "content-secondary");
assert_eq!(RowPart::Meta.intent(), "content-muted");
}
#[test]
fn a_token_part_carries_no_intent_of_its_own() {
assert_eq!(RowPart::Tokens.intent(), RowPart::Actions.intent());
assert_eq!(RowPart::Tokens.intent(), "content");
}
#[test]
fn a_separator_is_what_tells_a_section_from_a_subsection() {
assert!(Heading::Section.separated());
assert!(!Heading::Subsection.separated());
assert!(!Heading::Page.separated());
}
#[test]
fn a_chosen_segment_is_held_in_and_a_chosen_tab_comes_forward() {
assert_eq!(Selector::Segmented.chosen(), Depth::Well);
assert_eq!(Selector::Toggle.chosen(), Depth::Well);
assert_eq!(Selector::Tabs.chosen(), Depth::Raised);
assert_eq!(Selector::Segmented.chosen(), Depth::Raised.pressed());
assert!(Selector::Segmented.abutting());
assert!(Selector::Tabs.abutting());
assert!(!Selector::Toggle.abutting());
}
#[test]
fn a_pane_is_looked_into_and_a_band_is_not() {
assert_eq!(Region::Pane.depth(), Depth::Well);
assert_eq!(Region::Modal.depth(), Depth::Raised);
for r in [
Region::Band,
Region::Sidebar,
Region::Split,
Region::TabGroup,
] {
assert_eq!(r.depth(), Depth::Flat, "{r:?} should carry no edge");
}
}
#[test]
fn exactly_one_region_is_opaque() {
for r in [
Region::Band,
Region::Sidebar,
Region::Pane,
Region::Split,
Region::TabGroup,
Region::Modal,
] {
assert!(r.described(), "{r:?} should be describable");
}
assert!(!Region::Bespoke { name: "day-plan" }.described());
}
#[test]
fn a_bespoke_region_inherits_its_depth_rather_than_choosing_one() {
assert_eq!(Region::Bespoke { name: "day-plan" }.depth(), Depth::Flat);
assert_eq!(Region::Bespoke { name: "kanban" }.depth(), Depth::Flat);
}
#[test]
fn a_screen_with_a_bespoke_region_is_still_a_whole_screen() {
let day_plan = [
Region::Band,
Region::Bespoke { name: "day-plan" },
Region::Sidebar,
];
assert_eq!(day_plan.iter().filter(|r| r.described()).count(), 2);
assert_eq!(day_plan.iter().filter(|r| !r.described()).count(), 1);
}
#[test]
fn a_secret_field_is_marked_as_one_and_a_hidden_field_is_not_drawn() {
let secret = Field::new(FieldKind::Secret, "password", "Password");
assert!(secret.kind.confidential());
assert!(secret.kind.visible());
assert!(!FieldKind::Hidden.visible());
for k in [
FieldKind::Text,
FieldKind::Number,
FieldKind::Textarea,
FieldKind::Select,
FieldKind::Checkbox,
FieldKind::Hidden,
] {
assert!(!k.confidential(), "{k:?} should not be confidential");
}
assert!(FieldKind::Checkbox.labels_itself());
assert!(!FieldKind::Text.labels_itself());
}
#[test]
fn a_plain_field_offers_nothing_and_a_select_offers_its_options() {
let text = Field::new(FieldKind::Text, "title", "Title");
assert!(text.options.is_empty());
assert_eq!(text.placeholder, None);
let sizes = [Choice::plain("small"), Choice::plain("large")];
let select = Field::select("size", "Size", &sizes);
assert_eq!(select.kind, FieldKind::Select);
assert_eq!(select.options.len(), 2);
}
#[test]
fn a_choice_says_what_submits_and_what_is_read_apart() {
let plain = Choice::plain("7");
assert_eq!((plain.value, plain.label), ("7", "7"));
let spelled = Choice {
value: "7",
label: "One week",
};
assert_ne!(spelled.value, spelled.label);
}
#[test]
fn a_radio_asks_the_same_question_as_a_select_and_is_not_the_same_kind() {
let styles = [
Choice {
value: "copy",
label: "Copy samples in",
},
Choice {
value: "reference",
label: "Reference in place",
},
];
let radio = Field::radio("storage", "Storage style", &styles);
let select = Field::select("storage", "Storage style", &styles);
assert_eq!(radio.kind, FieldKind::Radio);
assert_ne!(radio.kind, select.kind);
assert_eq!(radio.options, select.options);
assert_eq!(
Field {
kind: select.kind,
..radio
},
select
);
}
#[test]
fn exactly_the_option_taking_kinds_say_so() {
assert!(FieldKind::Select.offers_options());
assert!(FieldKind::Radio.offers_options());
for kind in [
FieldKind::Text,
FieldKind::Secret,
FieldKind::Number,
FieldKind::Email,
FieldKind::Url,
FieldKind::Tel,
FieldKind::Textarea,
FieldKind::Checkbox,
FieldKind::Hidden,
] {
assert!(!kind.offers_options(), "{kind:?} does not offer options");
}
}
#[test]
fn a_radio_group_takes_a_label_even_though_its_options_carry_their_own() {
assert!(!FieldKind::Radio.labels_itself());
assert!(FieldKind::Checkbox.labels_itself());
}
#[test]
fn a_select_with_no_options_is_sayable() {
let loading = Field::select("project", "Project", &[]);
assert!(loading.options.is_empty());
}
#[test]
fn the_description_carries_the_question_and_never_the_answer() {
let f = Field {
placeholder: Some("yyyy-mm-dd"),
..Field::new(FieldKind::Text, "due", "Due")
};
assert_eq!(f.placeholder, Some("yyyy-mm-dd"));
assert_eq!(f.label, "Due");
}
#[test]
fn a_field_reports_its_own_error_state() {
let mut f = Field::new(FieldKind::Text, "title", "Title");
assert!(!f.invalid());
f.error = Some("Required");
assert!(f.invalid());
}
#[test]
fn columns_drop_by_priority_and_never_by_position() {
let cols = [
Column {
name: "Title",
width: Width::Fill,
priority: Priority::Essential,
},
Column {
name: "Due",
width: Width::Fixed,
priority: Priority::Secondary,
},
Column {
name: "Estimate",
width: Width::Fixed,
priority: Priority::Optional,
},
];
assert_eq!(
cols.iter()
.filter(|c| c.kept_at(Priority::Optional))
.count(),
3
);
let kept: Vec<_> = cols
.iter()
.filter(|c| c.kept_at(Priority::Secondary))
.map(|c| c.name)
.collect();
assert_eq!(kept, ["Title", "Due"]);
let kept: Vec<_> = cols
.iter()
.filter(|c| c.kept_at(Priority::Essential))
.map(|c| c.name)
.collect();
assert_eq!(kept, ["Title"]);
}
#[test]
fn inserting_a_column_does_not_move_what_gets_dropped() {
let before = [
Column::new("Title"),
Column {
name: "Estimate",
width: Width::Fixed,
priority: Priority::Optional,
},
];
let after = [
Column::new("Title"),
Column::new("Project"), Column {
name: "Estimate",
width: Width::Fixed,
priority: Priority::Optional,
},
];
fn dropped<'a>(cols: &[Column<'a>]) -> Vec<&'a str> {
cols.iter()
.filter(|c| !c.kept_at(Priority::Secondary))
.map(|c| c.name)
.collect()
}
assert_eq!(dropped(&before), ["Estimate"]);
assert_eq!(dropped(&after), ["Estimate"]);
}
#[test]
fn an_arrangement_carries_the_tab_group_as_a_modifier() {
let go = Arrangement::ListDetail { tabbed: true };
let plain = Arrangement::ListDetail { tabbed: false };
assert_ne!(go, plain);
assert_ne!(go, Arrangement::SidebarContent);
}
#[test]
fn readiness_names_the_state_and_not_the_shimmer() {
assert_ne!(Readiness::Ready, Readiness::Pending);
}
}