diff options
Diffstat (limited to '')
| -rw-r--r-- | Cargo.lock | 15 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | rocie-macros/.gitignore | 1 | ||||
| -rw-r--r-- | rocie-macros/Cargo.lock | 58 | ||||
| -rw-r--r-- | rocie-macros/Cargo.toml | 13 | ||||
| -rw-r--r-- | rocie-macros/src/form/generate.rs | 322 | ||||
| -rw-r--r-- | rocie-macros/src/form/mod.rs | 37 | ||||
| -rw-r--r-- | rocie-macros/src/form/parse.rs | 132 | ||||
| -rw-r--r-- | rocie-macros/src/lib.rs | 11 | ||||
| -rw-r--r-- | src/components/buy.rs | 23 | ||||
| -rw-r--r-- | src/components/form.rs | 120 | ||||
| -rw-r--r-- | src/components/input_placeholder.rs | 7 | ||||
| -rw-r--r-- | src/components/mod.rs | 11 | ||||
| -rw-r--r-- | src/components/select_placeholder.rs | 93 | ||||
| -rw-r--r-- | tailwind.config.js | 2 |
15 files changed, 709 insertions, 137 deletions
diff --git a/Cargo.lock b/Cargo.lock index ad62a73..2d545c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1576,9 +1576,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -1770,6 +1770,16 @@ dependencies = [ ] [[package]] +name = "rocie-macros" +version = "0.1.0" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "rocie-mobile" version = "0.1.0" dependencies = [ @@ -1784,6 +1794,7 @@ dependencies = [ "log", "reactive_stores", "rocie-client", + "rocie-macros", "uuid", "wasm-bindgen", "wasm-bindgen-test", diff --git a/Cargo.toml b/Cargo.toml index d1579b1..eb9e795 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ wasm-bindgen-test = "0.3" web-sys = { version = "0.3", features = ["Document", "Window"] } [dependencies] +rocie-macros = { path = "./rocie-macros/" } rocie-client = { path = "../rocie-server/crates/rocie-client" } leptos = { version = "0.8", features = ["csr"] } reactive_stores = { version = "0.2" } diff --git a/rocie-macros/.gitignore b/rocie-macros/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/rocie-macros/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rocie-macros/Cargo.lock b/rocie-macros/Cargo.lock new file mode 100644 index 0000000..dbce2cb --- /dev/null +++ b/rocie-macros/Cargo.lock @@ -0,0 +1,58 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rocie-macros" +version = "0.1.0" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" diff --git a/rocie-macros/Cargo.toml b/rocie-macros/Cargo.toml new file mode 100644 index 0000000..01e9e5c --- /dev/null +++ b/rocie-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rocie-macros" +version = "0.1.0" +edition = "2024" + +[dependencies] +proc-macro2 = "1.0.101" +quote = "1.0.41" +syn = {version = "2.0.106", features = []} +prettyplease = "0.2" + +[lib] +proc-macro = true diff --git a/rocie-macros/src/form/generate.rs b/rocie-macros/src/form/generate.rs new file mode 100644 index 0000000..6673ba5 --- /dev/null +++ b/rocie-macros/src/form/generate.rs @@ -0,0 +1,322 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{Ident, parse_macro_input}; + +use crate::form::{ParsedChild, ParsedInput}; + +pub fn form_impl(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ParsedInput); + + let node_refs = input.children.iter().map(node_ref); + let signals = input.children.iter().map(signal); + + let type_verifications = input.children.iter().map(type_verification); + + let output_struct_definition = output_struct_definition(&input.children); + let output_struct_construction = output_struct_construction(&input.children); + + let fetch_values = input.children.iter().map(fetch_value); + + let bounds = input.on_submit.inputs; + let on_submit_block = input.on_submit.block; + + let input_htmls = input.children.iter().map(input_html); + + let output = quote!({ + use crate::components::{ + input_placeholder::InputPlaceholder, + select_placeholder::SelectPlaceholder + }; + + use leptos::{ + view, + prelude::{ + Get, + NodeRef, + ElementChild, + ClassAttribute, + OnAttribute, + signal, + Set, + Show, + }, + html::{Input, Select}, + web_sys::SubmitEvent + }; + + use log::{info, error}; + + + #( + #node_refs + )* + #( + #signals + )* + #( + #type_verifications + )* + + let on_submit = move |ev: SubmitEvent| { + #output_struct_definition + + // stop the page from reloading! + ev.prevent_default(); + + #( + #fetch_values + )* + + let real_on_submit = |Inputs {#(#bounds),*}| #on_submit_block; + real_on_submit( + #output_struct_construction + ) + }; + + + view! { + <form class="flex flex-col contents-start m-2 g-2" on:submit=on_submit> + #( + #input_htmls + )* + + <div class="static"> + <input + type="submit" + value="Submit" + class="absolute bottom-0 right-0 h-20 w-20 rounded-lg bg-green-300 m-2" + /> + </div> + </form> + } + }); + + // { + // match syn::parse_file(quote! {fn main() { #output }}.to_string().as_str()) { + // Ok(tree) => { + // let formatted = prettyplease::unparse(&tree); + // eprint!("{}", formatted); + // } + // Err(err) => { + // eprintln!("Error: {err}\n{output}"); + // } + // }; + // } + + output.into() +} + +fn node_ref_name(name: &Ident) -> Ident { + format_ident!("{name}_node_ref") +} + +fn node_ref(child: &ParsedChild) -> TokenStream2 { + let (name, ty) = match child { + ParsedChild::Input { name, .. } => (name, quote! {Input}), + ParsedChild::Select { name, .. } => (name, quote! {Select}), + }; + + let node_ref_name = node_ref_name(name); + + quote! { + let #node_ref_name: NodeRef<#ty> = NodeRef::new(); + } +} + +fn signal_names(name: &Ident) -> (Ident, Ident) { + (format_ident!("{name}_get"), format_ident!("{name}_set")) +} +fn signal(child: &ParsedChild) -> TokenStream2 { + match child { + ParsedChild::Input { name, .. } => { + let (signal_name_get, signal_name_set) = signal_names(name); + + quote! { + let (#signal_name_get, #signal_name_set) = signal(None); + } + } + ParsedChild::Select { .. } => quote! {}, + } +} +fn type_verification(child: &ParsedChild) -> TokenStream2 { + match child { + ParsedChild::Input { .. } => quote! {}, + ParsedChild::Select { + rust_type, options, .. + } => { + let names: Vec<_> = options + .0 + .iter() + .enumerate() + .map(|(index, _)| format_ident!("name_{index}")) + .collect(); + let values: Vec<_> = options.0.iter().map(|option| option.1.clone()).collect(); + + quote! { + { + #( + let #names: #rust_type = #values; + )* + } + } + } + } +} + +fn output_struct_definition(children: &[ParsedChild]) -> TokenStream2 { + let names = children.iter().map(|child| match child { + ParsedChild::Input { name, .. } => name, + ParsedChild::Select { name, .. } => name, + }); + let rust_types = children.iter().map(|child| match child { + ParsedChild::Input { rust_type, .. } => rust_type, + ParsedChild::Select { rust_type, .. } => rust_type, + }); + + quote! { + struct Inputs { + #( + #names: #rust_types + ),* + } + } +} +fn output_struct_construction(children: &[ParsedChild]) -> TokenStream2 { + let names = children.iter().map(|child| match child { + ParsedChild::Input { name, .. } => name, + ParsedChild::Select { name, .. } => name, + }); + + quote! { + Inputs { + #( + #names + ),* + } + } +} + +fn fetch_value(child: &ParsedChild) -> TokenStream2 { + match child { + ParsedChild::Input { + name, rust_type, .. + } => { + let node_ref_name = node_ref_name(name); + let (_, signal_name_set) = signal_names(name); + + quote! { + let #name: #rust_type = { + let value = { + let output = #node_ref_name + .get() + // event handlers can only fire after the view + // is mounted to the DOM, so the `NodeRef` will be `Some` + .expect("<input> to exist") + .value(); + + let fin: Result<#rust_type, leptos::error::Error> = output + .parse() + .map_err(Into::<leptos::error::Error>::into); + fin + }; + + match value { + Ok(ok) => { + // Reset the signal + #signal_name_set.set(None); + + ok + } , + Err(err) => { + #signal_name_set.set(Some(err)); + + // Skip running the real `on_submit` + return + } + } + }; + } + } + ParsedChild::Select { + name, rust_type, .. + } => { + let node_ref_name = node_ref_name(name); + + quote! { + let #name: #rust_type = { + let value = { + let output = #node_ref_name + .get() + // event handlers can only fire after the view + // is mounted to the DOM, so the `NodeRef` will be `Some` + .expect("<input> to exist") + .value(); + + let fin: Result<#rust_type, leptos::error::Error> = output + .parse() + .map_err(Into::<leptos::error::Error>::into); + fin + }; + + match value { + Ok(ok) => { + ok + } , + Err(err) => { + unreachable!("Should be ruled out at compile time: {err}") + } + } + }; + } + } + } +} + +fn input_html(child: &ParsedChild) -> TokenStream2 { + match child { + ParsedChild::Input { + name, + label, + rust_type, + html_type, + .. + } => { + let node_ref_name = node_ref_name(name); + let (signal_name_get, _) = signal_names(name); + + quote! { + <InputPlaceholder input_type=#html_type label=#label node_ref=#node_ref_name /> + <Show + when=move || #signal_name_get.get().is_some() + fallback=|| () + > + <p class="ps-2 text-red-300">{move || + format!( + "Input is invalid for type {}: {}", + stringify!(#rust_type), + #signal_name_get.get().expect("Was `is_some`") + ) + }</p> + </Show> + } + } + ParsedChild::Select { + name, + label, + options, + .. + } => { + let node_ref_name = node_ref_name(name); + let options = options.0.iter().map(|(lit, expr)| { + quote! { + (#lit, #expr.to_string()) + } + }); + + quote! { + <SelectPlaceholder label=#label node_ref=#node_ref_name options=vec![#(#options),*] /> + } + } + } +} diff --git a/rocie-macros/src/form/mod.rs b/rocie-macros/src/form/mod.rs new file mode 100644 index 0000000..2719826 --- /dev/null +++ b/rocie-macros/src/form/mod.rs @@ -0,0 +1,37 @@ +use syn::{Expr, Ident, LitStr, Type}; + +mod parse; +mod generate; + +pub use generate::form_impl; + +#[derive(Debug)] +pub struct ParsedOnSubmit { + inputs: Vec<Ident>, + block: Expr, +} + +#[derive(Debug)] +pub enum ParsedChild { + Input { + name: Ident, + rust_type: Type, + html_type: LitStr, + label: LitStr, + }, + Select { + name: Ident, + label: LitStr, + rust_type: Type, + options: SelectOptions, + }, +} + +#[derive(Debug)] +pub struct SelectOptions(Vec<(LitStr, Expr)>); + +#[derive(Debug)] +pub struct ParsedInput { + on_submit: ParsedOnSubmit, + children: Vec<ParsedChild>, +} diff --git a/rocie-macros/src/form/parse.rs b/rocie-macros/src/form/parse.rs new file mode 100644 index 0000000..ef2087b --- /dev/null +++ b/rocie-macros/src/form/parse.rs @@ -0,0 +1,132 @@ +use quote::format_ident; +use syn::{bracketed, parenthesized, parse::{Parse, ParseStream}, Expr, Ident, LitStr, Token, Type}; + +use crate::form::{ParsedChild, ParsedInput, ParsedOnSubmit, SelectOptions}; + +macro_rules! parse_key_value { + ($input:expr, $name:ident as $ty:ty) => {{ + let key = $input.parse::<Ident>()?; + $input.parse::<Token![=]>()?; + let value = $input.parse::<$ty>()?; + + if key != format_ident!(stringify!($name)) { + panic!("Expected key name to be {}, but found: {key}", stringify!($name)); + } + + if $input.peek(Token![,]) { + $input.parse::<Token![,]>()?; + } + + value + }}; +} + +impl Parse for SelectOptions { + fn parse(input: ParseStream) -> syn::Result<Self> { + let content; + bracketed!(content in input); + let inner = content.parse_terminated( + |arg| { + let paren; + parenthesized!(paren in arg); + let lit = paren.parse::<LitStr>()?; + paren.parse::<Token![,]>()?; + let expr = paren.parse::<Expr>()?; + + Ok((lit, expr)) + }, + Token![,], + )?; + + Ok(Self(inner.into_iter().collect())) + } +} + +impl Parse for ParsedInput { + fn parse(input: ParseStream) -> syn::Result<Self> { + let on_submit = input.parse::<syn::Ident>()?; + + if on_submit != format_ident!("on_submit") { + panic!("Did not find correct `on_submit`: {on_submit}") + } + + input.parse::<Token![=]>()?; + + let on_submit = input.parse::<ParsedOnSubmit>()?; + + let mut children = Vec::new(); + while !input.is_empty() { + children.push(input.parse::<ParsedChild>()?); + } + + Ok(Self { + on_submit, + children, + }) + } +} + +impl Parse for ParsedChild { + fn parse(input: ParseStream) -> syn::Result<Self> { + input.parse::<Token![<]>()?; + + let variant = input.parse::<Ident>()?; + + let output = match variant { + variant if variant == format_ident!("Input") => { + let name = parse_key_value!(input, name as Ident); + let rust_type = parse_key_value!(input, rust_type as Type); + let html_type = parse_key_value!(input, html_type as LitStr); + let label = parse_key_value!(input, label as LitStr); + + ParsedChild::Input { + name, + rust_type, + html_type, + label, + } + } + variant if variant == format_ident!("Select") => { + let name = parse_key_value!(input, name as Ident); + let rust_type = parse_key_value!(input, rust_type as Type); + let label = parse_key_value!(input, label as LitStr); + let options = parse_key_value!(input, options as SelectOptions); + + ParsedChild::Select { + name, + label, + options, + rust_type, + } + } + _ => panic!("Unkown form child variant: {variant}"), + }; + + input.parse::<Token![/]>()?; + input.parse::<Token![>]>()?; + + Ok(output) + } +} + +impl Parse for ParsedOnSubmit { + fn parse(input: ParseStream) -> syn::Result<Self> { + let mut inputs = Vec::new(); + + input.parse::<Token![|]>()?; + while !input.peek(Token![|]) { + inputs.push(input.parse::<Ident>()?); + + if input.peek(Token![,]) { + input.parse::<Token![,]>()?; + } + } + input.parse::<Token![|]>()?; + + let block = input.parse::<Expr>()?; + + input.parse::<Token![;]>()?; + + Ok(Self { inputs, block }) + } +} diff --git a/rocie-macros/src/lib.rs b/rocie-macros/src/lib.rs new file mode 100644 index 0000000..4fb4340 --- /dev/null +++ b/rocie-macros/src/lib.rs @@ -0,0 +1,11 @@ +use proc_macro::TokenStream; + +use crate::form::form_impl; + +mod form; + +#[proc_macro] +#[allow(non_snake_case)] +pub fn Form(input: TokenStream) -> TokenStream { + form_impl(input) +} diff --git a/src/components/buy.rs b/src/components/buy.rs index 0c294ee..6d9402e 100644 --- a/src/components/buy.rs +++ b/src/components/buy.rs @@ -1,7 +1,9 @@ use leptos::{IntoView, component, view}; use log::info; +use rocie_client::models::UnitId; +use uuid::Uuid; -use crate::components::{form::Form, input_placeholder::InputPlaceholder, site_header::SiteHeader}; +use crate::components::{form::Form, site_header::SiteHeader}; #[component] pub fn Buy() -> impl IntoView { @@ -9,22 +11,27 @@ pub fn Buy() -> impl IntoView { <SiteHeader logo=icondata_io::IoPricetag back_location="/" name="Buy" /> {Form! { - on_submit = |Inputs {product_barcode, amount}| { - info!("Got product barcode: {product_barcode} with amount: {amount}"); - } + on_submit = |product_barcode, amount, unit_id| { + info!("Got product barcode: {product_barcode} with amount: {amount}, {unit_id}"); + }; <Input name=product_barcode, - signal_name_get=product_barcode_get, - signal_name_set=product_barcode_set, rust_type=u32, html_type="number", label="Product Barcode" /> + <Select + name=unit_id, + rust_type=Uuid, + label="Unit", + options=[ + ("Kilogram", Uuid::new_v4()), + ("Gram", Uuid::new_v4()) + ] + /> <Input name=amount, - signal_name_get=amount_get, - signal_name_set=amount_set, rust_type=u16, html_type="number", label="Amount" diff --git a/src/components/form.rs b/src/components/form.rs index fd55897..9c371ad 100644 --- a/src/components/form.rs +++ b/src/components/form.rs @@ -1,119 +1 @@ -macro_rules! Form { - ( - on_submit = |$bound:pat_param| $on_submit:block - $( - <Input - name=$name:ident, - signal_name_get=$signal_name_get:ident, - signal_name_set=$signal_name_set:ident, - rust_type=$rust_type:ty, - html_type=$input_type:literal, - label=$label:literal $(,)* - /> - )* - ) => {{ - use leptos::{ - view, - prelude::{ - Get, - NodeRef, - ElementChild, - ClassAttribute, - OnAttribute, - signal, - Set, - Show, - }, - html::Input, - web_sys::SubmitEvent - }; - - use log::info; - - - $( - let ($signal_name_get, $signal_name_set) = signal(None); - let $name: NodeRef<Input> = NodeRef::new(); - )* - - let on_submit = move |ev: SubmitEvent| { - struct Inputs { - $( - $name: $rust_type - ),* - } - - // stop the page from reloading! - ev.prevent_default(); - - $( - let value = { - let output = $name - .get() - // event handlers can only fire after the view - // is mounted to the DOM, so the `NodeRef` will be `Some` - .expect("<input> to exist") - .value(); - - let fin: Result<$rust_type, leptos::error::Error> = output - .parse() - .map_err(Into::<leptos::error::Error>::into); - fin - }; - - let $name = match value { - Ok(ok) => { - // Reset the signal - $signal_name_set.set(None); - - ok - } , - Err(err) => { - $signal_name_set.set(Some(err)); - - // Skip running the real `on_submit` - return - } - }; - )* - - let real_on_submit = |$bound| $on_submit; - real_on_submit(Inputs { - $( - $name - ),* - }) - }; - - - view! { - <form class="flex flex-col contents-start m-2 g-2" on:submit=on_submit> - $( - <InputPlaceholder input_type=$input_type label=$label node_ref=$name /> - <Show - when=move || $signal_name_get.get().is_some() - fallback=|| () - > - <p class="ps-2 text-red-300">{move || - format!( - "Input is invalid for type {}: {}", - stringify!($rust_type), - $signal_name_get.get().expect("Was `is_some`") - ) - }</p> - </Show> - )* - - <div class="static"> - <input - type="submit" - value="Submit" - class="absolute bottom-0 right-0 h-20 w-20 rounded-lg bg-green-300 m-2" - /> - </div> - </form> - } - }}; -} - -pub(crate) use Form; +pub(crate) use rocie_macros::Form; diff --git a/src/components/input_placeholder.rs b/src/components/input_placeholder.rs index 05b9509..aeef838 100644 --- a/src/components/input_placeholder.rs +++ b/src/components/input_placeholder.rs @@ -1,5 +1,3 @@ -use std::sync::atomic::{AtomicU32, Ordering}; - use leptos::{ IntoView, component, html::Input, @@ -7,11 +5,8 @@ use leptos::{ view, }; -fn get_id() -> u32 { - static ID: AtomicU32 = AtomicU32::new(0); +use crate::components::get_id; - ID.fetch_add(1, Ordering::Relaxed) -} #[component] pub fn InputPlaceholder( diff --git a/src/components/mod.rs b/src/components/mod.rs index f7b8dba..1ee37d5 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,9 +1,12 @@ +use std::sync::atomic::{AtomicU32, Ordering}; + // Generic pub mod async_fetch; pub mod container; +pub mod form; pub mod icon_p; pub mod input_placeholder; -pub mod form; +pub mod select_placeholder; // Specific pub mod buy; @@ -11,3 +14,9 @@ pub mod inventory; pub mod product_overview; pub mod recipies; pub mod site_header; + +fn get_id() -> u32 { + static ID: AtomicU32 = AtomicU32::new(0); + + ID.fetch_add(1, Ordering::Relaxed) +} diff --git a/src/components/select_placeholder.rs b/src/components/select_placeholder.rs new file mode 100644 index 0000000..947931c --- /dev/null +++ b/src/components/select_placeholder.rs @@ -0,0 +1,93 @@ +use leptos::{ + IntoView, + attr::{AttributeValue, IntoAttributeValue}, + component, + html::Select, + prelude::{ + ClassAttribute, CollectView, ElementChild, GlobalAttributes, NodeRef, NodeRefAttribute, + }, + view, +}; + +use crate::components::get_id; + +#[component] +pub fn SelectPlaceholder<T>( + label: &'static str, + node_ref: NodeRef<Select>, + options: Vec<(&'static str, T)>, +) -> impl IntoView +where + T: IntoAttributeValue, + <T as IntoAttributeValue>::Output: Send + AttributeValue, +{ + let id = get_id(); + + let options = options + .into_iter() + .map(|(label, value)| { + view! { + <option value=value>{label}</option> + } + }) + .collect_view(); + + view! { + <div class="relative h-14"> + <select + id=id.to_string() + class="\ + absolute \ + bottom-0 \ + bg-gray-200 \ + border-8 \ + border-b-2 \ + border-b-trasparent \ + border-gray-200 \ + focus:outline-none \ + h-10 \ + peer \ + placeholder-transparent \ + rounded-t-lg \ + text-gray-900 \ + w-full \ + " + node_ref=node_ref + > + {options} + </select> + + // TODO: Reference `var(--tw-border-2)` instead of the `2 px` <2025-10-01> + <div class=" + absolute \ + bottom-0 \ + h-[2px] \ + w-full \ + bg-gray-300 \ + peer-focus:bg-indigo-600 \ + " /> + + <label + for=id.to_string() + class="\ + bottom-10 \ + absolute \ + left-0 \ + text-gray-700 \ + text-sm \ + transition-all \ + peer-focus:bottom-10 \ + peer-focus:left-0 \ + peer-focus:text-gray-700 \ + peer-focus:text-sm \ + peer-placeholder-shown:text-base \ + peer-placeholder-shown:text-gray-400 \ + peer-placeholder-shown:bottom-2 \ + peer-placeholder-shown:left-2 \ + " + > + {label} + </label> + </div> + } +} diff --git a/tailwind.config.js b/tailwind.config.js index d4be7aa..d752bbe 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["*.html", "./src/**/*.rs"], + content: ["*.html", "./**/*.rs"], theme: { screens: { sm: "480px", |
