| 675 |
675 |
|
out
|
| 676 |
676 |
|
}
|
| 677 |
677 |
|
|
|
678 |
+ |
/// The cascade layer every stylesheet the make-family generates is wrapped in.
|
|
679 |
+ |
///
|
|
680 |
+ |
/// One name shared by every emitter in the family, so an app writes it once and
|
|
681 |
+ |
/// the design system's output lands in one place it can order against:
|
|
682 |
+ |
///
|
|
683 |
+ |
/// ```css
|
|
684 |
+ |
/// @layer makeover, base, components, responsive;
|
|
685 |
+ |
/// ```
|
|
686 |
+ |
///
|
|
687 |
+ |
/// # Why a layer at all
|
|
688 |
+ |
///
|
|
689 |
+ |
/// The cascade resolves origin and importance, then layer, then specificity,
|
|
690 |
+ |
/// then source order, and **unlayered normal declarations outrank every named
|
|
691 |
+ |
/// layer**. So the moment an app declares any layer of its own, every rule it
|
|
692 |
+ |
/// owns loses to unlayered generated CSS regardless of specificity or of
|
|
693 |
+ |
/// loading last. Emitting into a layer is what stops that, and putting the name
|
|
694 |
+ |
/// here rather than in each app is what stops three apps picking three names.
|
|
695 |
+ |
///
|
|
696 |
+ |
/// # Why this constant lives in the geometry crate
|
|
697 |
+ |
///
|
|
698 |
+ |
/// Not because spacing owns it. This crate is the only one every CSS-emitting
|
|
699 |
+ |
/// crate in the family already depends on, and it is already the crate that
|
|
700 |
+ |
/// spells CSS for the family (`media_condition`, `Step::token`, `Ratio::css`).
|
|
701 |
+ |
/// A second copy in `makeover-webview` is exactly the drift
|
|
702 |
+ |
/// [`Density::media_condition`] exists to prevent, one layer up.
|
|
703 |
+ |
pub const CSS_LAYER: &str = "makeover";
|
|
704 |
+ |
|
|
705 |
+ |
/// Wrap generated CSS in [`CSS_LAYER`].
|
|
706 |
+ |
///
|
|
707 |
+ |
/// Every whole-stylesheet emitter in the family ends with this call. Exposed
|
|
708 |
+ |
/// rather than kept private because an app that assembles its own stylesheet
|
|
709 |
+ |
/// out of this family's pieces has to put it in the same layer: goingson builds
|
|
710 |
+ |
/// `tables.css` in its own `build.rs` from `makeover_webview::list`, and those
|
|
711 |
+ |
/// rules are as generated as the ones in `layout.css`.
|
|
712 |
+ |
#[must_use]
|
|
713 |
+ |
pub fn in_css_layer(css: &str) -> String {
|
|
714 |
+ |
let mut out = format!("@layer {CSS_LAYER} {{\n");
|
|
715 |
+ |
for line in css.lines() {
|
|
716 |
+ |
// Blank lines stay blank; indenting one leaves trailing whitespace.
|
|
717 |
+ |
if line.is_empty() {
|
|
718 |
+ |
out.push('\n');
|
|
719 |
+ |
} else {
|
|
720 |
+ |
let _ = writeln!(out, " {line}");
|
|
721 |
+ |
}
|
|
722 |
+ |
}
|
|
723 |
+ |
out.push_str("}\n");
|
|
724 |
+ |
out
|
|
725 |
+ |
}
|
|
726 |
+ |
|
| 678 |
727 |
|
/// Emit the whole geometry layer as a `:root { … }` block at one density.
|
| 679 |
728 |
|
///
|
| 680 |
729 |
|
/// Mirrors `makeover::intent_css_vars`. Unlike the colour layer this is
|
| 708 |
757 |
|
/// `(hover: none)` alone, and audiofiles had no switch at all; the first of
|
| 709 |
758 |
|
/// those asked what device this is as a proxy for a capability already
|
| 710 |
759 |
|
/// reported.
|
|
760 |
+ |
///
|
|
761 |
+ |
/// Emitted inside [`CSS_LAYER`] since 0.6.0. Custom properties follow the
|
|
762 |
+ |
/// ordinary cascade, so unlayered ones outrank layered ones: an app that puts
|
|
763 |
+ |
/// its own `:root` overrides in a named layer while this file stayed unlayered
|
|
764 |
+ |
/// would find the generated tokens beating the overrides meant to replace them.
|
|
765 |
+ |
/// That is the same trap the component sheet had, and it is not visible until
|
|
766 |
+ |
/// the app adopts layers.
|
| 711 |
767 |
|
#[must_use]
|
| 712 |
768 |
|
pub fn density_css(explicit_touch: Option<&str>) -> String {
|
|
769 |
+ |
in_css_layer(&density_declarations(explicit_touch))
|
|
770 |
+ |
}
|
|
771 |
+ |
|
|
772 |
+ |
/// [`density_css`] without the layer wrapper.
|
|
773 |
+ |
fn density_declarations(explicit_touch: Option<&str>) -> String {
|
| 713 |
774 |
|
let mut css = geometry_css_vars(Density::Pointer);
|
| 714 |
775 |
|
css.push_str("\n/* Touch: targets separate, shells hold. */\n");
|
| 715 |
776 |
|
css.push_str("@media ");
|
| 758 |
819 |
|
assert!(!css.contains("ui-mode"), "a device mode crept in");
|
| 759 |
820 |
|
}
|
| 760 |
821 |
|
|
|
822 |
+ |
#[test]
|
|
823 |
+ |
fn the_spacing_layer_is_emitted_inside_the_family_layer() {
|
|
824 |
+ |
// Unlayered declarations outrank layered ones, so an app that layers
|
|
825 |
+ |
// its own :root overrides would lose to an unlayered geometry.css.
|
|
826 |
+ |
let css = density_css(None);
|
|
827 |
+ |
assert!(css.starts_with(&format!("@layer {CSS_LAYER} {{\n")));
|
|
828 |
+ |
assert!(css.trim_end().ends_with('}'));
|
|
829 |
+ |
// Everything still there, one level in.
|
|
830 |
+ |
assert!(css.contains(" :root {"));
|
|
831 |
+ |
assert!(css.contains("--gap-peer"));
|
|
832 |
+ |
}
|
|
833 |
+ |
|
|
834 |
+ |
#[test]
|
|
835 |
+ |
fn wrapping_leaves_no_trailing_whitespace_on_blank_lines() {
|
|
836 |
+ |
// A formatter strips these later and calls it a diff.
|
|
837 |
+ |
let css = in_css_layer("a {\n\nb\n}\n");
|
|
838 |
+ |
assert!(!css.lines().any(|l| l != l.trim_end()), "{css:?}");
|
|
839 |
+ |
}
|
|
840 |
+ |
|
| 761 |
841 |
|
#[test]
|
| 762 |
842 |
|
fn the_two_density_conditions_are_complements_and_not_negations() {
|
| 763 |
843 |
|
let pointer = Density::Pointer.media_condition();
|