tailwind_css_fixes/systems/preflight/
mod.rs

1use std::fmt::{Display, Formatter};
2
3/// Tailwind CSS Preflight v4 Style System
4/// <https://tailwindcss.com/docs/preflight>
5/// https://github.com/tailwindlabs/tailwindcss/blob/88b9f15b65588a87c5b6b13316530b4aecbc1b0b/packages/tailwindcss/preflight.css
6#[derive(Clone, Debug)]
7pub struct PreflightSystem {
8    /// Disables all preflight styles if set to true.
9    pub disable: bool,
10    /// Enables default CSS variables like  --default-transition-duration: 150ms;
11    pub default_vars: bool,
12    /// Resets box-sizing, margins, padding, and borders for all elements.
13    pub global_reset: bool,
14    /// Applies consistent line-height, font-family, and other root-level styles.
15    pub html_base: bool,
16    /// Unstyles headings (`h1`-`h6`) to inherit font size and weight.
17    pub unstyle_headings: bool,
18    /// Resets link colors and text decoration to inherit from their parent.
19    pub unstyle_links: bool,
20    /// Removes default list styles (`list-style: none`) and resets margin/padding.
21    pub unstyle_lists: bool,
22    /// Makes images and other replaced elements `display: block`.
23    pub block_level_media: bool,
24    /// Resets table styles for border-collapse and text-indent.
25    pub reset_tables: bool,
26    /// Applies a comprehensive reset to form elements like buttons, inputs, and textareas.
27    pub reset_forms: bool,
28    /// Prevents `hidden` elements from being displayed.
29    pub hidden_attribute: bool,
30
31    /// Includes minor typographic styles and element-specific enhancements.
32    /// This provides sensible defaults for elements like `<strong>`, `<code>`, `<abbr>`,
33    /// and fixes issues such as `<sub>` and `<sup>` affecting line height.
34    pub specific_extras: bool,
35
36    /// Includes a collection of hyper-specific browser compatibility fixes.
37    /// This is primarily focused on normalizing the appearance of complex form controls,
38    /// addressing quirks in WebKit date pickers, search inputs, number input buttons,
39    /// and Firefox's focus and invalid input styles.
40    pub compatibility_fixes: bool,
41    
42    /// User-defined custom CSS to be prepended to the preflight styles.
43    pub custom: String,
44}
45
46impl Default for PreflightSystem {
47    /// The default state includes the COMPLETE set of Preflight styles.
48    fn default() -> Self {
49        Self {
50            disable: false,
51            default_vars: true,
52            global_reset: true,
53            html_base: true,
54            unstyle_headings: true,
55            unstyle_links: true,
56            unstyle_lists: true,
57            block_level_media: true,
58            reset_tables: true,
59            reset_forms: true,
60            hidden_attribute: true,
61            specific_extras: true,
62            compatibility_fixes: true,
63            custom: String::new(),
64        }
65    }
66}
67
68impl PreflightSystem {
69  const DEFAULT_VARS: &'static str = r#"
70:root, :host {
71--font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
72  'Noto Color Emoji';
73--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
74  monospace;
75--ease-in: cubic-bezier(0.4, 0, 1, 1);
76--ease-out: cubic-bezier(0, 0, 0.2, 1);
77--default-transition-duration: 150ms;
78--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
79--default-font-family: var(--font-sans);
80--default-mono-font-family: var(--font-mono);
81@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
82  *, ::before, ::after, ::backdrop {
83    --tw-ease: initial;
84  }
85}
86
87}
88"#;
89    const GLOBAL_RESET: &'static str = r#"
90*,
91::after,
92::before,
93::backdrop,
94::file-selector-button {
95  box-sizing: border-box; /* 1 */
96  margin: 0; /* 2 */
97  padding: 0; /* 2 */
98  border: 0 solid; /* 3 */
99}
100"#;
101
102    const HTML_BASE: &'static str = r#"
103html,
104:host {
105  line-height: 1.5;
106  -webkit-text-size-adjust: 100%;
107  tab-size: 4;
108  font-family: --theme(
109    --default-font-family,
110    ui-sans-serif,
111    system-ui,
112    sans-serif,
113    'Apple Color Emoji',
114    'Segoe UI Emoji',
115    'Segoe UI Symbol',
116    'Noto Color Emoji'
117  );
118  font-feature-settings: --theme(--default-font-feature-settings, normal);
119  font-variation-settings: --theme(--default-font-variation-settings, normal);
120  -webkit-tap-highlight-color: transparent;
121}
122hr {
123  height: 0;
124  color: inherit;
125  border-top-width: 1px;
126}
127"#;
128
129    const UNSTYLE_HEADINGS: &'static str = r#"
130h1,
131h2,
132h3,
133h4,
134h5,
135h6 {
136  font-size: inherit;
137  font-weight: inherit;
138}
139"#;
140
141    const UNSTYLE_LINKS: &'static str = r#"
142a {
143  color: inherit;
144  -webkit-text-decoration: inherit;
145  text-decoration: inherit;
146}
147"#;
148
149    const UNSTYLE_LISTS: &'static str = r#"
150ol,
151ul,
152menu {
153  list-style: none;
154}
155"#;
156
157    const BLOCK_LEVEL_MEDIA: &'static str = r#"
158img,
159svg,
160video,
161canvas,
162audio,
163iframe,
164embed,
165object {
166  display: block;
167  vertical-align: middle;
168}
169img,
170video {
171  max-width: 100%;
172  height: auto;
173}
174"#;
175
176    const RESET_TABLES: &'static str = r#"
177table {
178  text-indent: 0;
179  border-color: inherit;
180  border-collapse: collapse;
181}
182"#;
183
184    const RESET_FORMS: &'static str = r#"
185button,
186input,
187select,
188optgroup,
189textarea,
190::file-selector-button {
191  font: inherit;
192  font-feature-settings: inherit;
193  font-variation-settings: inherit;
194  letter-spacing: inherit;
195  color: inherit;
196  border-radius: 0;
197  background-color: transparent;
198  opacity: 1;
199}
200::placeholder {
201  opacity: 1;
202}
203@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
204  ::placeholder {
205    color: color-mix(in oklab, currentcolor 50%, transparent);
206  }
207}
208textarea {
209  resize: vertical;
210}
211button,
212input:where([type='button'], [type='reset'], [type='submit']),
213::file-selector-button {
214  appearance: button;
215}
216"#;
217
218    const HIDDEN_ATTRIBUTE: &'static str = r#"
219[hidden]:where(:not([hidden='until-found'])) {
220  display: none !important;
221}
222"#;
223
224
225
226    const PREFLIGHT_EXTRAS: &'static str = r#"
227/* Minor typographic and element styles */
228abbr:where([title]) { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; }
229b, strong { font-weight: bolder; }
230code, kbd, samp, pre { font-family: --theme(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); font-size: 1em; }
231small { font-size: 80%; }
232sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
233sub { bottom: -0.25em; }
234sup { top: -0.5em; }
235progress { vertical-align: baseline; }
236summary { display: list-item; }
237"#;
238
239
240    const PREFLIGHT_COMPATIBILITY_FIXES: &'static str = r#"
241/* Browser-specific compatibility fixes */
242:-moz-focusring { outline: auto; }
243:-moz-ui-invalid { box-shadow: none; }
244:where(select:is([multiple], [size])) optgroup { font-weight: bolder; }
245:where(select:is([multiple], [size])) optgroup option { padding-inline-start: 20px; }
246::file-selector-button { margin-inline-end: 4px; }
247::-webkit-search-decoration { -webkit-appearance: none; }
248::-webkit-date-and-time-value { min-height: 1lh; text-align: inherit; }
249::-webkit-datetime-edit { display: inline-flex; }
250::-webkit-datetime-edit-fields-wrapper, ::-webkit-datetime-edit-fields-wrapper, ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { padding-block: 0; }
251::-webkit-calendar-picker-indicator { line-height: 1; }
252::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
253"#;
254
255
256
257    // Creates a Preflight instance with the COMPLETE set of Tailwind v4 styles.
258    pub fn full() -> Self {
259        Self::default()
260    }
261
262
263    /// Creates a Preflight instance without the "extras" and "compatibility fixes" styles.
264    pub fn core() -> Self {
265        Self {
266            specific_extras: false,
267            compatibility_fixes: false,
268            ..Self::default()
269        }
270    }
271
272    /// Appends custom CSS to the end of the preflight styles.
273    pub fn add_custom(&mut self, custom: &str) {
274        if !self.custom.is_empty() {
275            self.custom.push('\n');
276        }
277        self.custom.push_str(custom);
278    }
279  }
280
281
282impl Display for PreflightSystem {
283    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
284        if self.disable {
285            return Ok(());
286        }
287        
288        // Generate the built-in styles based on the boolean flags
289        if self.default_vars {
290            writeln!(f, "{}", Self::DEFAULT_VARS.trim())?;
291        }
292        if self.global_reset {
293            writeln!(f, "{}", Self::GLOBAL_RESET.trim())?;
294        }
295        if self.html_base {
296            writeln!(f, "{}", Self::HTML_BASE.trim())?;
297        }
298        if self.unstyle_headings {
299            writeln!(f, "{}", Self::UNSTYLE_HEADINGS.trim())?;
300        }
301        if self.unstyle_links {
302            writeln!(f, "{}", Self::UNSTYLE_LINKS.trim())?;
303        }
304        if self.unstyle_lists {
305            writeln!(f, "{}", Self::UNSTYLE_LISTS.trim())?;
306        }
307        if self.block_level_media {
308            writeln!(f, "{}", Self::BLOCK_LEVEL_MEDIA.trim())?;
309        }
310        if self.reset_tables {
311            writeln!(f, "{}", Self::RESET_TABLES.trim())?;
312        }
313        if self.reset_forms {
314            writeln!(f, "{}", Self::RESET_FORMS.trim())?;
315        }
316        if self.hidden_attribute {
317            writeln!(f, "{}", Self::HIDDEN_ATTRIBUTE.trim())?;
318        }
319        if self.specific_extras {
320            writeln!(f, "{}", Self::PREFLIGHT_EXTRAS.trim())?;
321        }
322        if self.compatibility_fixes {
323            writeln!(f, "{}", Self::PREFLIGHT_COMPATIBILITY_FIXES.trim())?;
324        }
325
326        // Append the user's custom styles at the very end
327        if !self.custom.is_empty() {
328            writeln!(f, "{}", self.custom.trim())?;
329        }
330
331        Ok(())
332    }
333}