diff --git a/CHANGELOG.md b/CHANGELOG.md index a4a9559af..1aee5c4c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure classes between `>` and `<` are properly extracted ([#17094](https://github.com/tailwindlabs/tailwindcss/pull/17094)) - Treat starting single quote as verbatim text in Slim ([#17085](https://github.com/tailwindlabs/tailwindcss/pull/17085)) - Ensure `.node` and `.wasm` files are not scanned for utilities ([#17123](https://github.com/tailwindlabs/tailwindcss/pull/17123)) -- Improve performance when scanning `JSON` files ([#17125](https://github.com/tailwindlabs/tailwindcss/pull/17125)) +- Improve performance when scanning JSON files ([#17125](https://github.com/tailwindlabs/tailwindcss/pull/17125)) +- Fix extracting candidates containing dots in Haml, Pug, and Slim pre processors ([#17113](https://github.com/tailwindlabs/tailwindcss/pull/17113)) - Don't create invalid CSS when encountering a link wrapped in square brackets ([#17129](https://github.com/tailwindlabs/tailwindcss/pull/17129)) ## [4.0.12] - 2025-03-07 diff --git a/crates/oxide/src/extractor/pre_processors/haml.rs b/crates/oxide/src/extractor/pre_processors/haml.rs index 4daed1c2b..37cb19054 100644 --- a/crates/oxide/src/extractor/pre_processors/haml.rs +++ b/crates/oxide/src/extractor/pre_processors/haml.rs @@ -1,6 +1,8 @@ use crate::cursor; use crate::extractor::bracket_stack::BracketStack; +use crate::extractor::machine::{Machine, MachineState}; use crate::extractor::pre_processors::pre_processor::PreProcessor; +use crate::extractor::variant_machine::VariantMachine; #[derive(Debug, Default)] pub struct Haml; @@ -14,29 +16,48 @@ impl PreProcessor for Haml { while cursor.pos < len { match cursor.curr { - // Consume strings as-is - b'\'' | b'"' => { - let len = cursor.input.len(); - let end_char = cursor.curr; + // Only replace `.` with a space if it's not surrounded by numbers. E.g.: + // + // ```diff + // - .flex.items-center + // + flex items-center + // ``` + // + // But with numbers, it's allowed: + // + // ```diff + // - px-2.5 + // + px-2.5 + // ``` + b'.' => { + // Don't replace dots with spaces when inside of any type of brackets, because + // this could be part of arbitrary values. E.g.: `bg-[url(https://example.com)]` + // ^ + if !bracket_stack.is_empty() { + cursor.advance(); + continue; + } - cursor.advance(); + // If the dot is surrounded by digits, we want to keep it. E.g.: `px-2.5` + // EXCEPT if it's followed by a valid variant that happens to start with a + // digit. + // E.g.: `bg-red-500.2xl:flex` + // ^^^ + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { + let mut next_cursor = cursor.clone(); + next_cursor.advance(); - while cursor.pos < len { - match cursor.curr { - // Escaped character, skip ahead to the next character - b'\\' => cursor.advance_twice(), - - // End of the string - b'\'' | b'"' if cursor.curr == end_char => break, - - // Everything else is valid - _ => cursor.advance(), - }; + let mut variant_machine = VariantMachine::default(); + if let MachineState::Done(_) = variant_machine.next(&mut next_cursor) { + result[cursor.pos] = b' '; + } + } else { + result[cursor.pos] = b' '; } } // Replace following characters with spaces if they are not inside of brackets - b'.' | b'#' | b'=' if bracket_stack.is_empty() => { + b'#' | b'=' if bracket_stack.is_empty() => { result[cursor.pos] = b' '; } @@ -48,7 +69,7 @@ impl PreProcessor for Haml { bracket_stack.push(cursor.curr); } - b')' | b']' | b'}' => { + b')' | b']' | b'}' if !bracket_stack.is_empty() => { bracket_stack.pop(cursor.curr); // Replace closing bracket with a space @@ -116,8 +137,56 @@ mod tests { ".text-lime-500.xl:text-emerald-500#root", " text-lime-500 xl:text-emerald-500 root", ), + // Dots in strings in HTML attributes stay as-is + (r#"
"#, r#""#), ] { Haml::test(input, expected); } } + + #[test] + fn test_strings_only_occur_when_nested() { + let input = r#" + %p.mt-2.text-xl + The quote in the next word, can't be the start of a string + + %h3.mt-24.text-center.text-4xl.font-bold.italic + The classes above should be extracted + "#; + + Haml::test_extract_contains( + input, + vec![ + // First paragraph + "mt-2", + "text-xl", + // second paragraph + "mt-24", + "text-center", + "text-4xl", + "font-bold", + "italic", + ], + ); + } + + // https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352 + #[test] + fn test_haml_full_file() { + let processed = Haml.process(include_bytes!("./test-fixtures/haml/src-1.haml")); + let actual = std::str::from_utf8(&processed).unwrap(); + let expected = include_str!("./test-fixtures/haml/dst-1.haml"); + + assert_eq!(actual, expected); + } + + #[test] + fn test_arbitrary_code_followed_by_classes() { + let input = r#" + %p + = i < 3 + .flex.items-center + "#; + Haml::test_extract_contains(input, vec!["flex", "items-center"]); + } } diff --git a/crates/oxide/src/extractor/pre_processors/pug.rs b/crates/oxide/src/extractor/pre_processors/pug.rs index eca30e3e8..ffee00c1d 100644 --- a/crates/oxide/src/extractor/pre_processors/pug.rs +++ b/crates/oxide/src/extractor/pre_processors/pug.rs @@ -1,6 +1,8 @@ use crate::cursor; use crate::extractor::bracket_stack::BracketStack; +use crate::extractor::machine::{Machine, MachineState}; use crate::extractor::pre_processors::pre_processor::PreProcessor; +use crate::extractor::variant_machine::VariantMachine; #[derive(Debug, Default)] pub struct Pug; @@ -14,30 +16,44 @@ impl PreProcessor for Pug { while cursor.pos < len { match cursor.curr { - // Consume strings as-is - b'\'' | b'"' => { - let len = cursor.input.len(); - let end_char = cursor.curr; - - cursor.advance(); - - while cursor.pos < len { - match cursor.curr { - // Escaped character, skip ahead to the next character - b'\\' => cursor.advance_twice(), - - // End of the string - b'\'' | b'"' if cursor.curr == end_char => break, - - // Everything else is valid - _ => cursor.advance(), - }; + // Only replace `.` with a space if it's not surrounded by numbers. E.g.: + // + // ```diff + // - .flex.items-center + // + flex items-center + // ``` + // + // But with numbers, it's allowed: + // + // ```diff + // - px-2.5 + // + px-2.5 + // ``` + b'.' => { + // Don't replace dots with spaces when inside of any type of brackets, because + // this could be part of arbitrary values. E.g.: `bg-[url(https://example.com)]` + // ^ + if !bracket_stack.is_empty() { + cursor.advance(); + continue; } - } - // Replace dots with spaces - b'.' if bracket_stack.is_empty() => { - result[cursor.pos] = b' '; + // If the dot is surrounded by digits, we want to keep it. E.g.: `px-2.5` + // EXCEPT if it's followed by a valid variant that happens to start with a + // digit. + // E.g.: `bg-red-500.2xl:flex` + // ^^^ + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { + let mut next_cursor = cursor.clone(); + next_cursor.advance(); + + let mut variant_machine = VariantMachine::default(); + if let MachineState::Done(_) = variant_machine.next(&mut next_cursor) { + result[cursor.pos] = b' '; + } + } else { + result[cursor.pos] = b' '; + } } b'(' | b'[' | b'{' => { @@ -77,8 +93,45 @@ mod tests { "bg-[url(https://example.com/?q=[1,2])]", "bg-[url(https://example.com/?q=[1,2])]", ), + // Classes in HTML attributes + (r#""#, r#""#), ] { Pug::test(input, expected); } } + + #[test] + fn test_strings_only_occur_when_nested() { + let input = r#" + p.mt-2.text-xl + div The quote in the next word, can't be the start of a string + + h3.mt-24.text-center.text-4xl.font-bold.italic + div The classes above should be extracted + "#; + + Pug::test_extract_contains( + input, + vec![ + // First paragraph + "mt-2", + "text-xl", + // second paragraph + "mt-24", + "text-center", + "text-4xl", + "font-bold", + "italic", + ], + ); + } + + #[test] + fn test_arbitrary_code_followed_by_classes() { + let input = r#" + - i < 3 + .flex.items-center + "#; + Pug::test_extract_contains(input, vec!["flex", "items-center"]); + } } diff --git a/crates/oxide/src/extractor/pre_processors/slim.rs b/crates/oxide/src/extractor/pre_processors/slim.rs index 6352a6358..6b7008863 100644 --- a/crates/oxide/src/extractor/pre_processors/slim.rs +++ b/crates/oxide/src/extractor/pre_processors/slim.rs @@ -1,6 +1,8 @@ use crate::cursor; use crate::extractor::bracket_stack::BracketStack; +use crate::extractor::machine::{Machine, MachineState}; use crate::extractor::pre_processors::pre_processor::PreProcessor; +use crate::extractor::variant_machine::VariantMachine; #[derive(Debug, Default)] pub struct Slim; @@ -11,50 +13,47 @@ impl PreProcessor for Slim { let mut result = content.to_vec(); let mut cursor = cursor::Cursor::new(content); let mut bracket_stack = BracketStack::default(); - let mut line_start_pos = 0x00; while cursor.pos < len { match cursor.curr { - b'\n' => { - line_start_pos = cursor.pos + 1; - } - - // Line indicators: + // Only replace `.` with a space if it's not surrounded by numbers. E.g.: // - // > Verbatim text with trailing white space ' - // > See: https://github.com/slim-template/slim?tab=readme-ov-file#verbatim-text-with-trailing-white-space- - b'\'' - if cursor.input[line_start_pos..cursor.pos] - .iter() - .all(|x| x.is_ascii_whitespace()) => - { - // Do not treat the `'` as a string - } - - // Consume strings as-is - b'\'' | b'"' => { - let len = cursor.input.len(); - let end_char = cursor.curr; - - cursor.advance(); - - while cursor.pos < len { - match cursor.curr { - // Escaped character, skip ahead to the next character - b'\\' => cursor.advance_twice(), - - // End of the string - b'\'' | b'"' if cursor.curr == end_char => break, - - // Everything else is valid - _ => cursor.advance(), - }; + // ```diff + // - .flex.items-center + // + flex items-center + // ``` + // + // But with numbers, it's allowed: + // + // ```diff + // - px-2.5 + // + px-2.5 + // ``` + b'.' => { + // Don't replace dots with spaces when inside of any type of brackets, because + // this could be part of arbitrary values. E.g.: `bg-[url(https://example.com)]` + // ^ + if !bracket_stack.is_empty() { + cursor.advance(); + continue; } - } - // Replace dots with spaces - b'.' if bracket_stack.is_empty() => { - result[cursor.pos] = b' '; + // If the dot is surrounded by digits, we want to keep it. E.g.: `px-2.5` + // EXCEPT if it's followed by a valid variant that happens to start with a + // digit. + // E.g.: `bg-red-500.2xl:flex` + // ^^^ + if cursor.prev.is_ascii_digit() && cursor.next.is_ascii_digit() { + let mut next_cursor = cursor.clone(); + next_cursor.advance(); + + let mut variant_machine = VariantMachine::default(); + if let MachineState::Done(_) = variant_machine.next(&mut next_cursor) { + result[cursor.pos] = b' '; + } + } else { + result[cursor.pos] = b' '; + } } // Any `[` preceded by an alphanumeric value will not be part of a candidate. @@ -111,6 +110,11 @@ mod tests { // Convert dots to spaces ("div.flex.bg-red-500", "div flex bg-red-500"), (".flex.bg-red-500", " flex bg-red-500"), + (".bg-red-500.2xl:flex", " bg-red-500 2xl:flex"), + ( + ".bg-red-500.2xl:flex.bg-green-200.3xl:flex", + " bg-red-500 2xl:flex bg-green-200 3xl:flex", + ), // Keep dots in strings (r#"div(class="px-2.5")"#, r#"div(class="px-2.5")"#), // Replace top-level `(a-z0-9)[` with `$1 `. E.g.: `.flex[x]` -> `.flex x]` @@ -135,6 +139,12 @@ mod tests { ("content-['50[]']", "content-['50[]']"), // Escaped string ("content-['a\'b\'c\'']", "content-['a\'b\'c\'']"), + // Classes in HTML attributes + ("", ""), + ( + "", + "", + ), ] { Slim::test(input, expected); } @@ -155,20 +165,6 @@ mod tests { | Should be BLACK on RED - FAIL "#; - let expected = r#" - text-black - data-controller= ['foo', ('bar' if rand.positive?)].join(' ') - ] - bg-green-300 - | BLACK on GREEN - OK - - bg-red-300 - data-foo= 42 - ] - | Should be BLACK on RED - FAIL - "#; - - Slim::test(input, expected); Slim::test_extract_contains(input, vec!["text-black", "bg-green-300", "bg-red-300"]); } @@ -197,4 +193,39 @@ mod tests { Slim::test(input, expected); Slim::test_extract_contains(input, vec!["text-red-500", "text-3xl"]); } + + #[test] + fn test_strings_only_occur_when_nested() { + let input = r#" + p.mt-2.text-xl + | The quote in the next word, can't be the start of a string + + h3.mt-24.text-center.text-4xl.font-bold.italic + | The classes above should be extracted + "#; + + Slim::test_extract_contains( + input, + vec![ + // First paragraph + "mt-2", + "text-xl", + // second paragraph + "mt-24", + "text-center", + "text-4xl", + "font-bold", + "italic", + ], + ); + } + + #[test] + fn test_arbitrary_code_followed_by_classes() { + let input = r#" + - i < 3 + .flex.items-center + "#; + Slim::test_extract_contains(input, vec!["flex", "items-center"]); + } } diff --git a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-1.haml b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-1.haml new file mode 100644 index 000000000..fd8bd6eaf --- /dev/null +++ b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/dst-1.haml @@ -0,0 +1,155 @@ +/ https://github com/tailwindlabs/tailwindcss/pull/17051 issuecomment-2711181352 +- star_styles "size- 800px mask mask-star bg-gradient-to-r from-secondary via-cyan-400 to-lime-400" +- crazy_text_styles "italic font-black bg-gradient-to-r via-60% to-90% from-orange-500 via-secondary to-primary text-transparent bg-clip-text inline-block py-2" + + relative + - Blurred background star + absolute left-0 z-0 class: "-top-[400px] -right-[400px]" + flex justify-end blur-3xl + %div class: star_styles + + relative z-10 + %h1 mt-8 text-center text-5xl font-black tracking-wide drop-shadow-lg + %div + Components, Guides, and Paradigms + md:inline-flex md:items-center justify-center + %span -mr-2 + for + %span relative -rotate-3 hover:-rotate-6 hover:scale-125 transition-transform + %span class: crazy_text_styles + " absolute blur-xl" + CRAZY + %span class: crazy_text_styles + " drop-shadow-[0_0_1px_#fff]" + CRAZY + %span -ml-3 md:-ml-1 + \-fast Development + %div + in Ruby on Rails! + + mt-16 text-center + daisy_button "😃 Get Started", css: "btn-primary text-xl", + right_icon: "arrow-right", target: "_blank", + href: "https://github com/profoundry-us/loco_motion locomotion-components" + + mt-32 lg:flex lg:space-x-8 + %div class: "md:basis-2/5" + %h3 text-4xl font-bold + Easy, Flexible Components + %p mt-2 text-xl + Powered by the fabulous + succeed ',' do + daisy_link "ViewComponent", "https://viewcomponent.org", target: "_blank" + succeed ', and' do + daisy_link "DaisyUI", "https://daisyui com/", target: "_blank" + daisy_link "TailwindCSS", "https://tailwindcss com/", target: "_blank" + libraries, our components are designed to be fast, flexible, and easy to + use directy in Ruby on Rails! + + doc_example css: "mt-8 lg:mt-0 lg:basis-3/5 h-44" do + flex flex-col sm:flex-row items-center gap-4 + daisy_button "Accent Button", css: "btn-accent" + daisy_tip "Click to Swap" do + daisy_swap off: "🌚", on: "🌞", css: "swap-rotate text-4xl" + daisy_badge "Large Badge", css: "badge-secondary badge-lg" + + + mt-32 xl:flex xl:flex-row-reverse xl:items-center + text-xl xl:ml-8 + %h3 text-4xl font-bold + Simple, Concise Views + + %p mt-2 + Utlize + daisy_link "HAML", "https://haml.info/", target: "_blank" + so your views are simple, concise, and easy to understand + + %p mt-2 + No more messy ERB files with all of their closing tags and Ruby wrappers + HAML feels more natural to write and reduces file sizes, making your + views easier to read and maintain + + %p mt-2 + :markdown + **PLUS!** You can utlize filters like _Markdown_, CoffeeScript, + Textile, and many more! + + mt-8 xl:mt-0 + flex flex-col xl:flex xl:flex-row w-full + doc_code css: "grow", code_css: "xl:pl-2 xl:pr-0 xl:rounded-r-none", language: "erb" do + :escaped + <% Ruby %> + + <% 5 times do |i| %> + <% if i even? %> +Number <% i %>
+ <% else %> +Numero <% i %>
+ <% end %> + <% end %> + + doc_code css: "grow mt-8 xl:mt-0", pre_css: "xl:h-full", code_css: "xl:pl-0 xl:pr-2 xl:h-full xl:rounded-l-none" do + :escaped + - HAML + + - 5 times do |i| + - if i even? + %p odd Number \ i + - else + %p even Numero \ i + + + %h3 mt-32 text-4xl font-bold + Build Your OWN Components + + %p mt-2 text-xl + Can't find exactly what you need? No problem! Build your own components + with ease using our simple, flexible, and powerful DSL + + mt-8 xl:flex xl:space-x-8 + %div + doc_code language: "ruby" do + :escaped + app/components/application_component rb + class ApplicationComponent < LocoMotion::BaseComponent + Add your custom / shared component logic here! + end + + doc_code language: "haml", css: "mt-8" do + :escaped + - app/components/character_component html haml + part :component do + part :head + part :body do + content + part :legs + + %div mt-8 xl:mt-0 + doc_code language: "ruby" do + :escaped + app/components/character_component rb + class CharacterComponent < ApplicationComponent + define_parts :head, :body, :legs + + def before_render + set_tag_name :head, :h1 + add_css :head, "text-3xl font-bold" + + set_tag_name :body, :p + add_stimulus_controller :body, "character-body" + add_css :body, "text-lg" + + set_tag_name :legs, :footer + add_css :legs, "text-sm" + end + end + + %h3 mt-24 text-center text-4xl font-bold italic + More Coming Soon! + %p mt-2 text-xl text-center + Keen an eye out as we'll be adding more components, guides, and + %br max-sm:hidden + suggested gems for you to build amazing Rails apps! + mt-4 text-center + daisy_button "😉 Get Started", css: "btn-primary text-xl", + right_icon: "arrow-right", target: "_blank", + class: "px-2.5" + href: "https://github com/profoundry-us/loco_motion locomotion-components" diff --git a/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/src-1.haml b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/src-1.haml new file mode 100644 index 000000000..38e17b134 --- /dev/null +++ b/crates/oxide/src/extractor/pre_processors/test-fixtures/haml/src-1.haml @@ -0,0 +1,155 @@ +/ https://github.com/tailwindlabs/tailwindcss/pull/17051#issuecomment-2711181352 +- star_styles = "size-[800px] mask mask-star bg-gradient-to-r from-secondary via-cyan-400 to-lime-400" +- crazy_text_styles = "italic font-black bg-gradient-to-r via-60% to-90% from-orange-500 via-secondary to-primary text-transparent bg-clip-text inline-block py-2" + +.relative + - # Blurred background star + .absolute.left-0.z-0{ class: "-top-[400px] -right-[400px]" } + .flex.justify-end.blur-3xl + %div{ class: star_styles } + + .relative.z-10 + %h1.mt-8.text-center.text-5xl.font-black.tracking-wide.drop-shadow-lg + %div + Components, Guides, and Paradigms + .md:inline-flex.md:items-center.justify-center + %span.-mr-2 + for + %span.relative.-rotate-3.hover:-rotate-6.hover:scale-125.transition-transform + %span{ class: crazy_text_styles + " absolute blur-xl" } + CRAZY + %span{ class: crazy_text_styles + " drop-shadow-[0_0_1px_#fff]" } + CRAZY + %span.-ml-3.md:-ml-1 + \-fast Development + %div + in Ruby on Rails! + + .mt-16.text-center + = daisy_button "😃 Get Started", css: "btn-primary text-xl", + right_icon: "arrow-right", target: "_blank", + href: "https://github.com/profoundry-us/loco_motion#locomotion-components" + + .mt-32.lg:flex.lg:space-x-8 + %div{ class: "md:basis-2/5" } + %h3.text-4xl.font-bold + Easy, Flexible Components + %p.mt-2.text-xl + Powered by the fabulous + = succeed ',' do + = daisy_link("ViewComponent", "https://viewcomponent.org", target: "_blank") + = succeed ', and' do + = daisy_link "DaisyUI", "https://daisyui.com/", target: "_blank" + = daisy_link "TailwindCSS", "https://tailwindcss.com/", target: "_blank" + libraries, our components are designed to be fast, flexible, and easy to + use directy in Ruby on Rails! + + = doc_example(css: "mt-8 lg:mt-0 lg:basis-3/5 h-44") do + .flex.flex-col.sm:flex-row.items-center.gap-4 + = daisy_button "Accent Button", css: "btn-accent" + = daisy_tip("Click to Swap") do + = daisy_swap off: "🌚", on: "🌞", css: "swap-rotate text-4xl" + = daisy_badge "Large Badge", css: "badge-secondary badge-lg" + + + .mt-32.xl:flex.xl:flex-row-reverse.xl:items-center + .text-xl.xl:ml-8 + %h3.text-4xl.font-bold + Simple, Concise Views + + %p.mt-2 + Utlize + = daisy_link("HAML", "https://haml.info/", target: "_blank") + so your views are simple, concise, and easy to understand. + + %p.mt-2 + No more messy ERB files with all of their closing tags and Ruby wrappers. + HAML feels more natural to write and reduces file sizes, making your + views easier to read and maintain. + + %p.mt-2 + :markdown + **PLUS!** You can utlize filters like _Markdown_, CoffeeScript, + Textile, and many more! + + .mt-8.xl:mt-0 + .flex.flex-col.xl:flex.xl:flex-row.w-full + = doc_code(css: "grow", code_css: "xl:pl-2 xl:pr-0 xl:rounded-r-none", language: "erb") do + :escaped + <% # Ruby %> + + <% 5.times do |i| %> + <% if i.even? %> +Number <%= i %>
+ <% else %> +Numero <%= i %>
+ <% end %> + <% end %> + + = doc_code(css: "grow mt-8 xl:mt-0", pre_css: "xl:h-full", code_css: "xl:pl-0 xl:pr-2 xl:h-full xl:rounded-l-none") do + :escaped + - # HAML + + - 5.times do |i| + - if i.even? + %p.odd Number \#{i} + - else + %p.even Numero \#{i} + + + %h3.mt-32.text-4xl.font-bold + Build Your OWN Components + + %p.mt-2.text-xl + Can't find exactly what you need? No problem! Build your own components + with ease using our simple, flexible, and powerful DSL. + + .mt-8.xl:flex.xl:space-x-8 + %div + = doc_code(language: "ruby") do + :escaped + # app/components/application_component.rb + class ApplicationComponent < LocoMotion::BaseComponent + # Add your custom / shared component logic here! + end + + = doc_code(language: "haml", css: "mt-8") do + :escaped + - # app/components/character_component.html.haml + = part(:component) do + = part(:head) + = part(:body) do + = content + = part(:legs) + + %div.mt-8.xl:mt-0 + = doc_code(language: "ruby") do + :escaped + # app/components/character_component.rb + class CharacterComponent < ApplicationComponent + define_parts :head, :body, :legs + + def before_render + set_tag_name(:head, :h1) + add_css(:head, "text-3xl font-bold") + + set_tag_name(:body, :p) + add_stimulus_controller(:body, "character-body") + add_css(:body, "text-lg") + + set_tag_name(:legs, :footer) + add_css(:legs, "text-sm") + end + end + + %h3.mt-24.text-center.text-4xl.font-bold.italic + More Coming Soon! + %p.mt-2.text-xl.text-center + Keen an eye out as we'll be adding more components, guides, and + %br.max-sm:hidden + suggested gems for you to build amazing Rails apps! + .mt-4.text-center + = daisy_button "😉 Get Started", css: "btn-primary text-xl", + right_icon: "arrow-right", target: "_blank", + class: "px-2.5" + href: "https://github.com/profoundry-us/loco_motion#locomotion-components"