ferro_json_ui/assets/mod.rs
1//! Embedded static assets for ferro-json-ui.
2//!
3//! Served by the framework via the automatically-registered
4//! `GET /_ferro/ferro-base.css` route. Embedded at compile time —
5//! no runtime file I/O.
6
7pub(crate) mod quill;
8
9pub mod leaflet;
10
11/// Pre-built Tailwind CSS covering every utility class emitted by
12/// ferro-json-ui components.
13///
14/// Regenerate with `scripts/gen-ferro-base-css.sh` after adding or
15/// modifying components that introduce new utility classes.
16///
17/// In release builds (no `dev-css` feature): static compile-time constant.
18/// In dev builds (`dev-css` feature): use `ferro_base_css()` instead (disk read).
19#[cfg(not(feature = "dev-css"))]
20pub const FERRO_BASE_CSS: &str = include_str!("../../assets/ferro-base.css");
21
22/// Returns the pre-built ferro-json-ui base CSS.
23///
24/// In release builds (no `dev-css` feature): returns the compile-time embedded
25/// constant as a `Cow::Borrowed` — zero allocation, zero I/O.
26///
27/// In dev builds (`dev-css` feature): reads `assets/ferro-base.css` from disk
28/// at call time so that a running Tailwind `--watch` process can update the CSS
29/// without a Rust recompile. Panics if the file is unreadable.
30pub fn ferro_base_css() -> std::borrow::Cow<'static, str> {
31 #[cfg(not(feature = "dev-css"))]
32 {
33 std::borrow::Cow::Borrowed(FERRO_BASE_CSS)
34 }
35 #[cfg(feature = "dev-css")]
36 {
37 let css = std::fs::read_to_string(concat!(
38 env!("CARGO_MANIFEST_DIR"),
39 "/assets/ferro-base.css"
40 ))
41 .expect("ferro-base.css must be readable in dev-css mode");
42 std::borrow::Cow::Owned(css)
43 }
44}
45
46/// Geist Sans Variable woff2. Served at `/_ferro/fonts/Geist-Variable.woff2`.
47///
48/// Source: vercel/geist-font v1.7.2, OFL-1.1 license.
49pub const GEIST_SANS_WOFF2: &[u8] = include_bytes!("../../assets/fonts/Geist-Variable.woff2");
50
51/// Geist Mono Variable woff2. Served at `/_ferro/fonts/GeistMono-Variable.woff2`.
52///
53/// Source: vercel/geist-font v1.7.2, OFL-1.1 license.
54pub const GEIST_MONO_WOFF2: &[u8] = include_bytes!("../../assets/fonts/GeistMono-Variable.woff2");
55
56/// OFL-1.1 license text for the Geist fonts.
57///
58/// Served at `/_ferro/fonts/OFL.txt` alongside the font files.
59pub const GEIST_OFL_TXT: &[u8] = include_bytes!("../../assets/fonts/OFL.txt");
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 #[allow(clippy::const_is_empty)]
67 fn ferro_base_css_non_empty() {
68 let css = ferro_base_css();
69 assert!(!css.is_empty(), "embedded CSS must not be empty");
70 // include_str! guarantees valid UTF-8 (compile error otherwise),
71 // so runtime validation is unnecessary. Smoke-check a class that
72 // every ferro-json-ui page relies on:
73 assert!(
74 css.contains("flex"),
75 "expected `flex` utility in generated CSS"
76 );
77 }
78
79 #[test]
80 #[allow(clippy::const_is_empty)]
81 fn geist_sans_woff2_non_empty() {
82 assert!(
83 !GEIST_SANS_WOFF2.is_empty(),
84 "Geist Sans woff2 must not be empty"
85 );
86 }
87
88 #[test]
89 #[allow(clippy::const_is_empty)]
90 fn geist_mono_woff2_non_empty() {
91 assert!(
92 !GEIST_MONO_WOFF2.is_empty(),
93 "Geist Mono woff2 must not be empty"
94 );
95 }
96
97 #[test]
98 fn geist_sans_woff2_magic_bytes() {
99 // WOFF2 magic number: 'w' 'O' 'F' '2' (0x77 0x4F 0x46 0x32)
100 assert_eq!(
101 &GEIST_SANS_WOFF2[..4],
102 &[0x77, 0x4F, 0x46, 0x32],
103 "Geist Sans woff2 must start with wOF2 magic bytes"
104 );
105 }
106
107 #[test]
108 fn geist_mono_woff2_magic_bytes() {
109 assert_eq!(
110 &GEIST_MONO_WOFF2[..4],
111 &[0x77, 0x4F, 0x46, 0x32],
112 "Geist Mono woff2 must start with wOF2 magic bytes"
113 );
114 }
115
116 #[test]
117 fn ferro_base_css_contains_motion_duration_fallback() {
118 let css = ferro_base_css();
119 // SC1: v1 themes that omit --motion-duration-fast resolve via the fallback.
120 assert!(
121 css.contains("var(--motion-duration-fast,"),
122 "expected motion-duration-fast fallback in generated CSS; run scripts/gen-ferro-base-css.sh"
123 );
124 // The duration utilities must exist as class rules — a theme-layer
125 // variable emission alone satisfies the var() substring above without
126 // any consumable utility being generated.
127 for class in [".duration-fast{", ".duration-base{", ".duration-slow{"] {
128 assert!(
129 css.contains(class),
130 "expected `{class}` utility rule in generated CSS; run scripts/gen-ferro-base-css.sh"
131 );
132 }
133 // SC3: reduced-motion collapse survives regeneration. The collapse
134 // declarations need !important — theme <style> tags are injected
135 // after this stylesheet and redeclare the tokens on :root at equal
136 // specificity, so normal declarations would lose on source order.
137 assert!(
138 css.contains("prefers-reduced-motion"),
139 "expected prefers-reduced-motion block in generated CSS"
140 );
141 assert!(
142 css.contains("--motion-duration-fast:.01ms!important"),
143 "expected !important on the reduced-motion collapse; run scripts/gen-ferro-base-css.sh"
144 );
145 }
146
147 #[test]
148 fn ferro_base_css_ring_falls_back_to_primary() {
149 let css = ferro_base_css();
150 // 23-slot v1 themes never define --color-ring; focus rings must
151 // resolve to the theme's primary color instead of an undefined var.
152 assert!(
153 css.contains("var(--color-ring,var(--color-primary))"),
154 "expected --color-ring fallback to --color-primary in generated CSS; run scripts/gen-ferro-base-css.sh"
155 );
156 }
157
158 // ── SKIN-04: dev-css hot-reload path ──────────────────────────────────────
159
160 /// Release mode (no dev-css): ferro_base_css() must return Cow::Borrowed
161 /// referencing the compile-time embedded constant.
162 ///
163 /// Cow::Borrowed is the only zero-allocation/zero-I/O variant — the test
164 /// uses matches! on the discriminant to distinguish it from Cow::Owned.
165 #[test]
166 #[cfg(not(feature = "dev-css"))]
167 fn ferro_base_css_release_returns_borrowed() {
168 use std::borrow::Cow;
169 let css = ferro_base_css();
170 assert!(
171 matches!(css, Cow::Borrowed(_)),
172 "without dev-css feature, ferro_base_css() must return Cow::Borrowed (zero allocation)"
173 );
174 // Also verify it equals the embedded constant so no divergence can sneak in.
175 assert_eq!(
176 css.as_ref(),
177 FERRO_BASE_CSS,
178 "Cow::Borrowed content must equal the embedded FERRO_BASE_CSS constant"
179 );
180 }
181
182 /// Dev mode (dev-css feature): ferro_base_css() must return Cow::Owned
183 /// whose content equals the on-disk assets/ferro-base.css file.
184 ///
185 /// The file is read independently (via CARGO_MANIFEST_DIR) to prove the
186 /// function actually went to disk rather than returning the embedded bytes.
187 /// This test does NOT mutate the file (parallel-safe).
188 #[test]
189 #[cfg(feature = "dev-css")]
190 fn ferro_base_css_dev_returns_owned_matching_disk() {
191 use std::borrow::Cow;
192 let css = ferro_base_css();
193 assert!(
194 matches!(css, Cow::Owned(_)),
195 "with dev-css feature, ferro_base_css() must return Cow::Owned (disk read)"
196 );
197 // Read the file independently to prove the content matches.
198 let disk_content = std::fs::read_to_string(concat!(
199 env!("CARGO_MANIFEST_DIR"),
200 "/assets/ferro-base.css"
201 ))
202 .expect("assets/ferro-base.css must exist and be readable for the dev-css test");
203 assert_eq!(
204 css.as_ref(),
205 disk_content.as_str(),
206 "Cow::Owned content must equal the on-disk ferro-base.css (disk read proves hot-reload path is wired)"
207 );
208 }
209}