├── .gitignore
├── Cargo.toml
├── README.md
├── styled
├── Cargo.toml
├── README.md
├── rust-toolchain
└── src
│ └── lib.rs
└── styled_macro
├── .gitignore
├── Cargo.toml
├── rust-toolchain
└── src
└── lib.rs
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 |
3 | members = [
4 | "styled",
5 | ]
6 |
7 | [workspace.dependencies]
8 | styled = { path = "./styled" }
9 | styled_macro = { path = "./styled_macro" }
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Styled: Easy Styling for Leptos Components
2 |
3 | If you're looking for an easy way to apply scoped styles to your [`Leptos`](https://github.com/leptos-rs/leptos) components, `Styled` is the Leptos macro you need. With `Styled`, you can apply high-level selectors like `button` or `div` to specific components, keeping your markup clean and organized.
4 |
5 | ## Installation
6 |
7 | Use `cargo add` in your project root
8 |
9 | ```bash
10 | cargo add styled stylist
11 | ```
12 |
13 | Then make sure that your `Cargo.toml` is properly configured, adding the feature flags for Styled
14 |
15 | ```toml
16 | [features]
17 | csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr", "styled/csr"]
18 | ssr = [
19 | "dep:actix-files",
20 | "dep:actix-web",
21 | "dep:leptos_actix",
22 | "leptos/ssr",
23 | "leptos_meta/ssr",
24 | "leptos_router/ssr",
25 | "stylist/ssr",
26 | "styled/ssr",
27 | ]
28 | ```
29 |
30 | ## Usage
31 |
32 | First create a basic `Leptos` component. This will serve as the foundation for this little guide.
33 |
34 | ```rust
35 | #[component]
36 | pub fn MyComponent(cx: Scope) -> impl IntoView{
37 | view! {
38 | cx,
39 |
"hello"
40 | }
41 | }
42 | ```
43 |
44 | Next, import the `style` macro, powered by an awesome crate called [`Stylist`](https://github.com/futursolo/stylist-rs), to create your styles.
45 | Just add this to the top of your file.
46 |
47 | ```rust
48 | use styled::style;
49 | ```
50 |
51 | You can then use the `style` macro to create a `Result` containing your styles. Let's modify our component:
52 |
53 | ```rust
54 | #[component]
55 | pub fn MyComponent(cx: Scope) -> impl IntoView{
56 |
57 | let styles = style!(
58 | div {
59 | background-color: red;
60 | color: white;
61 | }
62 | );
63 |
64 | view! {
65 | cx,
66 | "hello"
67 | }
68 | }
69 | ```
70 |
71 | Now, let's apply those styles with our `styled::view!` macro!
72 |
73 | ```rust
74 | #[component]
75 | pub fn MyComponent(cx: Scope) -> impl IntoView {
76 |
77 | let styles = style!(
78 | div {
79 | background-color: red;
80 | color: white;
81 | }
82 | );
83 |
84 | styled::view! {
85 | cx,
86 | styles,
87 | "This text should be red with white text."
88 | }
89 | }
90 | ```
91 |
92 | Now we can define another component that also uses the `div` CSS selector but it's styles will only apply to the elements inside of it's enclosing `styled::view!` macro.
93 |
94 | ```rust
95 | #[component]
96 | pub fn AnotherComponent(cx: Scope) -> impl IntoView {
97 |
98 | // note were using a plain div selector and it wont clash with MyComponent's div style!
99 | let styles = style!(
100 | div {
101 | background-color: blue;
102 | color: gray;
103 | }
104 | );
105 |
106 | styled::view! {
107 | cx,
108 | styles,
109 | "This text should be blue with gray text."
110 | }
111 | }
112 | ```
113 |
114 | ## Longer Example
115 |
116 | ```rust
117 | // /src/components/button.rs
118 |
119 | use crate::theme::get_theme;
120 | use leptos::*;
121 | use styled::style;
122 |
123 | #[derive(PartialEq)]
124 | pub enum Variant {
125 | PRIMARY,
126 | SECONDARY,
127 | ALERT,
128 | DISABLED,
129 | }
130 |
131 | impl Variant {
132 | pub fn is(&self, variant: &Variant) -> bool {
133 | self == variant
134 | }
135 | }
136 |
137 | struct ButtonColors {
138 | text: String,
139 | background: String,
140 | border: String,
141 | }
142 |
143 | fn get_colors(variant: &Variant) -> ButtonColors {
144 | let theme = get_theme().unwrap();
145 | match variant {
146 | Variant::PRIMARY => ButtonColors {
147 | text: theme.white(),
148 | background: theme.black(),
149 | border: theme.transparent(),
150 | },
151 | Variant::SECONDARY => ButtonColors {
152 | text: theme.black(),
153 | background: theme.white(),
154 | border: theme.gray.lightest(),
155 | },
156 | Variant::ALERT => ButtonColors {
157 | text: theme.white(),
158 | background: theme.red(),
159 | border: theme.transparent(),
160 | },
161 | Variant::DISABLED => ButtonColors {
162 | text: theme.white(),
163 | background: theme.red(),
164 | border: theme.transparent(),
165 | },
166 | }
167 | }
168 |
169 | #[component]
170 | pub fn Button(cx: Scope, variant: Variant) -> impl IntoView {
171 | let disabled = variant.is(&Variant::DISABLED);
172 |
173 | let styles = styles(&variant);
174 |
175 | styled::view! {
176 | cx,
177 | styles,
178 | "Button"
179 | }
180 | }
181 |
182 | fn styles<'a>(variant: &Variant) -> styled::Result {
183 | let colors = get_colors(variant);
184 |
185 | style!(
186 | button {
187 | color: ${colors.text};
188 | background-color: ${colors.background};
189 | border: 1px solid ${colors.border};
190 | outline: none;
191 | height: 48px;
192 | min-width: 154px;
193 | font-size: 14px;
194 | font-weight: 700;
195 | text-align: center;
196 | box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
197 | position: relative;
198 | box-sizing: border-box;
199 | vertical-align: middle;
200 | text-align: center;
201 | text-overflow: ellipsis;
202 | text-transform: uppercase;
203 | overflow: hidden;
204 | cursor: pointer;
205 | transition: box-shadow 0.2s;
206 | margin: 10px;
207 | }
208 |
209 | & button:active {
210 | transform: scale(0.99);
211 | }
212 |
213 |
214 | & button::-moz-focus-inner {
215 | border: none;
216 | }
217 |
218 | & button::before {
219 | content: "";
220 | position: absolute;
221 | top: 0;
222 | bottom: 0;
223 | left: 0;
224 | right: 0;
225 | background-color: rgb(255, 255, 255);
226 | opacity: 0;
227 | transition: opacity 0.2s;
228 | }
229 |
230 | & button::after {
231 | content: "";
232 | position: absolute;
233 | left: 50%;
234 | top: 50%;
235 | border-radius: 50%;
236 | padding: 50%;
237 | background-color: ${colors.text};
238 | opacity: 0;
239 | transform: translate(-50%, -50%) scale(1);
240 | transition: opacity 1s, transform 0.5s;
241 | }
242 |
243 | & button:hover,
244 | & button:focus {
245 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
246 | }
247 |
248 | & button:hover::before {
249 | opacity: 0.08;
250 | }
251 |
252 | & button:hover:focus::before {
253 | opacity: 0.3;
254 | }
255 |
256 | & button:active {
257 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
258 | }
259 |
260 | & button:active::after {
261 | opacity: 0.32;
262 | transform: translate(-50%, -50%) scale(0);
263 | transition: transform 0s;
264 | }
265 |
266 | & button:disabled {
267 | color: rgba(0, 0, 0, 0.28);
268 | background-color: rgba(0, 0, 0, 0.12);
269 | box-shadow: none;
270 | cursor: initial;
271 | }
272 |
273 | & button:disabled::before {
274 | opacity: 0;
275 | }
276 |
277 | & button:disabled::after {
278 | opacity: 0;
279 | }
280 |
281 | )
282 | }
283 |
284 | ```
285 |
286 | ```rust
287 | // /src/theme/mod.rs
288 | use csscolorparser::Color;
289 |
290 | pub fn get_theme() -> Result {
291 | let theme = Theme {
292 | teal: Colors {
293 | main: Color::from_html("#6FDDDB")?,
294 | darker: Color::from_html("#2BB4B2")?,
295 | lighter: Color::from_html("#7EE1DF")?,
296 | lightest: Color::from_html("#B2EDEC")?,
297 | },
298 | pink: Colors {
299 | main: Color::from_html("#E93EF5")?,
300 | darker: Color::from_html("#C70BD4")?,
301 | lighter: Color::from_html("#F5A4FA")?,
302 | lightest: Color::from_html("#FCE1FD")?,
303 | },
304 | green: Colors {
305 | main: Color::from_html("#54D072")?,
306 | darker: Color::from_html("#30AF4F")?,
307 | lighter: Color::from_html("#82DD98")?,
308 | lightest: Color::from_html("#B4EAC1")?,
309 | },
310 | purple: Colors {
311 | main: Color::from_html("#8C18FB")?,
312 | darker: Color::from_html("#7204DB")?,
313 | lighter: Color::from_html("#B162FC")?,
314 | lightest: Color::from_html("#D0A1FD")?,
315 | },
316 | yellow: Colors {
317 | main: Color::from_html("#E1E862")?,
318 | darker: Color::from_html("#BAC31D")?,
319 | lighter: Color::from_html("#EFF3AC")?,
320 | lightest: Color::from_html("#FAFBE3")?,
321 | },
322 | gray: Colors {
323 | main: Color::from_html("#4a4a4a")?,
324 | darker: Color::from_html("#3d3d3d")?,
325 | lighter: Color::from_html("#939393")?,
326 | lightest: Color::from_html("#c4c4c4")?,
327 | },
328 | red: Color::from_html("#FF5854")?,
329 | black: Color::from_html("#000000")?,
330 | white: Color::from_html("#FFFFFF")?,
331 | transparent: Color::from_html("transparent")?,
332 | };
333 |
334 | Ok(theme)
335 | }
336 |
337 | pub struct Theme {
338 | pub teal: Colors,
339 | pub pink: Colors,
340 | pub green: Colors,
341 | pub purple: Colors,
342 | pub yellow: Colors,
343 | pub gray: Colors,
344 | pub red: Color,
345 | pub black: Color,
346 | pub white: Color,
347 | pub transparent: Color,
348 | }
349 |
350 | pub struct Colors {
351 | pub main: Color,
352 | pub darker: Color,
353 | pub lighter: Color,
354 | pub lightest: Color,
355 | }
356 |
357 | impl Colors {
358 | pub fn main(&self) -> String {
359 | self.main.to_hex_string()
360 | }
361 | pub fn darker(&self) -> String {
362 | self.darker.to_hex_string()
363 | }
364 | pub fn lighter(&self) -> String {
365 | self.lighter.to_hex_string()
366 | }
367 | pub fn lightest(&self) -> String {
368 | self.lightest.to_hex_string()
369 | }
370 | }
371 |
372 | impl Theme {
373 | pub fn red(&self) -> String {
374 | self.red.to_hex_string()
375 | }
376 | pub fn black(&self) -> String {
377 | self.black.to_hex_string()
378 | }
379 | pub fn white(&self) -> String {
380 | self.white.to_hex_string()
381 | }
382 | pub fn transparent(&self) -> String {
383 | self.transparent.to_hex_string()
384 | }
385 | }
386 |
387 |
388 | ```
389 |
390 | ```rust
391 | // /src/app.rs
392 |
393 | #[component]
394 | fn HomePage(cx: Scope) -> impl IntoView {
395 | view! { cx,
396 |
397 |
398 |
399 | }
400 | }
401 | ```
402 |
--------------------------------------------------------------------------------
/styled/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "styled"
3 | version = "0.2.0"
4 | edition = "2021"
5 | description = "Scoped styles for your Leptos components"
6 | license = "APL-1.0"
7 | keywords = ["leptos", "scoped", "styles", "styling", "CSS"]
8 | categories = ["wasm", "web-programming"]
9 | readme = "README.md"
10 | homepage = "https://github.com/eboody/styled"
11 |
12 | [features]
13 | csr = [
14 | "leptos/csr",
15 | "leptos_meta/csr"
16 | ]
17 | ssr = [
18 | "leptos/ssr",
19 | "leptos_meta/ssr",
20 | "stylist/ssr"
21 | ]
22 |
23 | [dependencies]
24 | leptos = { version = "0.5.4", features = ["nightly"] }
25 | leptos_meta = { version = "0.5.4", features = ["nightly"] }
26 | regex = "1.9.3"
27 | stylist = "0.13"
28 |
--------------------------------------------------------------------------------
/styled/README.md:
--------------------------------------------------------------------------------
1 | # Styled: Easy Styling for Leptos Components
2 |
3 | If you're looking for an easy way to apply scoped styles to your [`Leptos`](https://github.com/leptos-rs/leptos) components, `Styled` is the Leptos macro you need. With `Styled`, you can apply high-level selectors like `button` or `div` to specific components, keeping your markup clean and organized.
4 |
5 | ## Installation
6 |
7 | Use `cargo add` in your project root
8 |
9 | ```bash
10 | cargo add styled stylist
11 | ```
12 |
13 | ## Usage
14 |
15 | First create a basic `Leptos` component. This will serve as the foundation for this little guide.
16 |
17 | ```rust
18 | #[component]
19 | pub fn MyComponent(cx: Scope) -> impl IntoView{
20 | view! {
21 | cx,
22 | "hello"
23 | }
24 | }
25 | ```
26 |
27 | Next, import the `style` macro, powered by an awesome crate called [`Stylist`](https://github.com/futursolo/stylist-rs), to create your styles.
28 | Just add this to the top of your file.
29 |
30 | ```rust
31 | use styled::style;
32 | ```
33 |
34 | You can then use the `style` macro to create a `Result` containing your styles. Let's modify our component:
35 |
36 | ```rust
37 | #[component]
38 | pub fn MyComponent(cx: Scope) -> impl IntoView{
39 |
40 | let styles = style!(
41 | div {
42 | background-color: red;
43 | color: white;
44 | }
45 | );
46 |
47 | view! {
48 | cx,
49 | "hello"
50 | }
51 | }
52 | ```
53 |
54 | Now, let's apply those styles with our `styled::view!` macro!
55 |
56 | ```rust
57 | #[component]
58 | pub fn MyComponent(cx: Scope) -> impl IntoView {
59 |
60 | let styles = style!(
61 | div {
62 | background-color: red;
63 | color: white;
64 | }
65 | );
66 |
67 | styled::view! {
68 | cx,
69 | styles,
70 | "This text should be red with white text."
71 | }
72 | }
73 | ```
74 |
75 | Now we can define another component that also uses the `div` CSS selector but it's styles will only apply to the elements inside of it's enclosing `styled::view!` macro.
76 |
77 | ```rust
78 | #[component]
79 | pub fn AnotherComponent(cx: Scope) -> impl IntoView {
80 |
81 | // note were using a plain div selector and it wont clash with MyComponent's div style!
82 | let styles = style!(
83 | div {
84 | background-color: blue;
85 | color: gray;
86 | }
87 | );
88 |
89 | styled::view! {
90 | cx,
91 | styles,
92 | "This text should be blue with gray text."
93 | }
94 | }
95 | ```
96 |
97 | ## Longer Example
98 |
99 | ```rust
100 | // /src/components/button.rs
101 |
102 | use crate::theme::get_theme;
103 | use leptos::*;
104 | use styled::style;
105 |
106 | #[derive(PartialEq)]
107 | pub enum Variant {
108 | PRIMARY,
109 | SECONDARY,
110 | ALERT,
111 | DISABLED,
112 | }
113 |
114 | impl Variant {
115 | pub fn is(&self, variant: &Variant) -> bool {
116 | self == variant
117 | }
118 | }
119 |
120 | struct ButtonColors {
121 | text: String,
122 | background: String,
123 | border: String,
124 | }
125 |
126 | fn get_colors(variant: &Variant) -> ButtonColors {
127 | let theme = get_theme().unwrap();
128 | match variant {
129 | Variant::PRIMARY => ButtonColors {
130 | text: theme.white(),
131 | background: theme.black(),
132 | border: theme.transparent(),
133 | },
134 | Variant::SECONDARY => ButtonColors {
135 | text: theme.black(),
136 | background: theme.white(),
137 | border: theme.gray.lightest(),
138 | },
139 | Variant::ALERT => ButtonColors {
140 | text: theme.white(),
141 | background: theme.red(),
142 | border: theme.transparent(),
143 | },
144 | Variant::DISABLED => ButtonColors {
145 | text: theme.white(),
146 | background: theme.red(),
147 | border: theme.transparent(),
148 | },
149 | }
150 | }
151 |
152 | #[component]
153 | pub fn Button(cx: Scope, variant: Variant) -> impl IntoView {
154 | let disabled = variant.is(&Variant::DISABLED);
155 |
156 | let styles = styles(&variant);
157 |
158 | styled::view! {
159 | cx,
160 | styles,
161 | "Button"
162 | }
163 | }
164 |
165 | fn styles<'a>(variant: &Variant) -> styled::Result {
166 | let colors = get_colors(variant);
167 |
168 | style!(
169 | button {
170 | color: ${colors.text};
171 | background-color: ${colors.background};
172 | border: 1px solid ${colors.border};
173 | outline: none;
174 | height: 48px;
175 | min-width: 154px;
176 | font-size: 14px;
177 | font-weight: 700;
178 | text-align: center;
179 | box-shadow: rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
180 | position: relative;
181 | box-sizing: border-box;
182 | vertical-align: middle;
183 | text-align: center;
184 | text-overflow: ellipsis;
185 | text-transform: uppercase;
186 | overflow: hidden;
187 | cursor: pointer;
188 | transition: box-shadow 0.2s;
189 | margin: 10px;
190 | }
191 |
192 | & button:active {
193 | transform: scale(0.99);
194 | }
195 |
196 |
197 | & button::-moz-focus-inner {
198 | border: none;
199 | }
200 |
201 | & button::before {
202 | content: "";
203 | position: absolute;
204 | top: 0;
205 | bottom: 0;
206 | left: 0;
207 | right: 0;
208 | background-color: rgb(255, 255, 255);
209 | opacity: 0;
210 | transition: opacity 0.2s;
211 | }
212 |
213 | & button::after {
214 | content: "";
215 | position: absolute;
216 | left: 50%;
217 | top: 50%;
218 | border-radius: 50%;
219 | padding: 50%;
220 | background-color: ${colors.text};
221 | opacity: 0;
222 | transform: translate(-50%, -50%) scale(1);
223 | transition: opacity 1s, transform 0.5s;
224 | }
225 |
226 | & button:hover,
227 | & button:focus {
228 | box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12);
229 | }
230 |
231 | & button:hover::before {
232 | opacity: 0.08;
233 | }
234 |
235 | & button:hover:focus::before {
236 | opacity: 0.3;
237 | }
238 |
239 | & button:active {
240 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
241 | }
242 |
243 | & button:active::after {
244 | opacity: 0.32;
245 | transform: translate(-50%, -50%) scale(0);
246 | transition: transform 0s;
247 | }
248 |
249 | & button:disabled {
250 | color: rgba(0, 0, 0, 0.28);
251 | background-color: rgba(0, 0, 0, 0.12);
252 | box-shadow: none;
253 | cursor: initial;
254 | }
255 |
256 | & button:disabled::before {
257 | opacity: 0;
258 | }
259 |
260 | & button:disabled::after {
261 | opacity: 0;
262 | }
263 |
264 | )
265 | }
266 |
267 | ```
268 |
269 | ```rust
270 | // /src/theme/mod.rs
271 | use csscolorparser::Color;
272 |
273 | pub fn get_theme() -> Result {
274 | let theme = Theme {
275 | teal: Colors {
276 | main: Color::from_html("#6FDDDB")?,
277 | darker: Color::from_html("#2BB4B2")?,
278 | lighter: Color::from_html("#7EE1DF")?,
279 | lightest: Color::from_html("#B2EDEC")?,
280 | },
281 | pink: Colors {
282 | main: Color::from_html("#E93EF5")?,
283 | darker: Color::from_html("#C70BD4")?,
284 | lighter: Color::from_html("#F5A4FA")?,
285 | lightest: Color::from_html("#FCE1FD")?,
286 | },
287 | green: Colors {
288 | main: Color::from_html("#54D072")?,
289 | darker: Color::from_html("#30AF4F")?,
290 | lighter: Color::from_html("#82DD98")?,
291 | lightest: Color::from_html("#B4EAC1")?,
292 | },
293 | purple: Colors {
294 | main: Color::from_html("#8C18FB")?,
295 | darker: Color::from_html("#7204DB")?,
296 | lighter: Color::from_html("#B162FC")?,
297 | lightest: Color::from_html("#D0A1FD")?,
298 | },
299 | yellow: Colors {
300 | main: Color::from_html("#E1E862")?,
301 | darker: Color::from_html("#BAC31D")?,
302 | lighter: Color::from_html("#EFF3AC")?,
303 | lightest: Color::from_html("#FAFBE3")?,
304 | },
305 | gray: Colors {
306 | main: Color::from_html("#4a4a4a")?,
307 | darker: Color::from_html("#3d3d3d")?,
308 | lighter: Color::from_html("#939393")?,
309 | lightest: Color::from_html("#c4c4c4")?,
310 | },
311 | red: Color::from_html("#FF5854")?,
312 | black: Color::from_html("#000000")?,
313 | white: Color::from_html("#FFFFFF")?,
314 | transparent: Color::from_html("transparent")?,
315 | };
316 |
317 | Ok(theme)
318 | }
319 |
320 | pub struct Theme {
321 | pub teal: Colors,
322 | pub pink: Colors,
323 | pub green: Colors,
324 | pub purple: Colors,
325 | pub yellow: Colors,
326 | pub gray: Colors,
327 | pub red: Color,
328 | pub black: Color,
329 | pub white: Color,
330 | pub transparent: Color,
331 | }
332 |
333 | pub struct Colors {
334 | pub main: Color,
335 | pub darker: Color,
336 | pub lighter: Color,
337 | pub lightest: Color,
338 | }
339 |
340 | impl Colors {
341 | pub fn main(&self) -> String {
342 | self.main.to_hex_string()
343 | }
344 | pub fn darker(&self) -> String {
345 | self.darker.to_hex_string()
346 | }
347 | pub fn lighter(&self) -> String {
348 | self.lighter.to_hex_string()
349 | }
350 | pub fn lightest(&self) -> String {
351 | self.lightest.to_hex_string()
352 | }
353 | }
354 |
355 | impl Theme {
356 | pub fn red(&self) -> String {
357 | self.red.to_hex_string()
358 | }
359 | pub fn black(&self) -> String {
360 | self.black.to_hex_string()
361 | }
362 | pub fn white(&self) -> String {
363 | self.white.to_hex_string()
364 | }
365 | pub fn transparent(&self) -> String {
366 | self.transparent.to_hex_string()
367 | }
368 | }
369 |
370 |
371 | ```
372 |
373 | ```rust
374 | // /src/app.rs
375 |
376 | #[component]
377 | fn HomePage(cx: Scope) -> impl IntoView {
378 | view! { cx,
379 |
380 |
381 |
382 | }
383 | }
384 | ```
385 |
386 |
--------------------------------------------------------------------------------
/styled/rust-toolchain:
--------------------------------------------------------------------------------
1 | nightly
2 |
--------------------------------------------------------------------------------
/styled/src/lib.rs:
--------------------------------------------------------------------------------
1 | // pub use styled_macro::view;
2 | use regex::Regex;
3 | use stylist::{Result, Style as Styles};
4 |
5 | pub use leptos::*;
6 | use leptos_dom::HydrationCtx;
7 | pub use leptos_meta::Style;
8 |
9 | pub use stylist::style;
10 |
11 | #[macro_export]
12 | macro_rules! view {
13 | ($styles:expr, $($tokens:tt)*) => {{
14 |
15 | let style = $styles;
16 |
17 | let $crate::StyleInfo { class_name, style_string } = $crate::get_style_info(style);
18 | use $crate::Style;
19 | ::leptos::view! {
20 | class={class_name.clone()},
21 |
22 | $($tokens)*
23 | }
24 | }};
25 | }
26 |
27 | pub fn get_style_info(styles_result: Result) -> StyleInfo {
28 | let hydration_context_id = HydrationCtx::peek_always();
29 |
30 | let style_struct = styles_result.unwrap();
31 |
32 | let class_name = String::from("styled-") + &hydration_context_id.to_string();
33 |
34 | let style_string = style_struct.get_style_str().to_owned();
35 |
36 | style_struct.unregister();
37 |
38 | let re = Regex::new(r"stylist-\w+").unwrap();
39 |
40 | let style_string = re.replace_all(&style_string, &class_name);
41 |
42 | let re = Regex::new(r"(\.styled(-\d+)+) (-?[_a-zA-Z\.#~]+[_a-zA-Z0-9-]*+)").unwrap();
43 |
44 | let regex_to_fix_stylist_bug = Regex::new(r"(\dpx)([-])").unwrap();
45 |
46 | let style_string_with_fixed_pixels = regex_to_fix_stylist_bug
47 | .replace_all(&style_string, "$1 $2")
48 | .to_string();
49 |
50 | let new_style_string = re
51 | .replace_all(&style_string_with_fixed_pixels, "$3$1")
52 | .to_string();
53 |
54 | StyleInfo {
55 | class_name,
56 | style_string: new_style_string,
57 | }
58 | }
59 |
60 | #[derive(Clone)]
61 | pub struct StyleInfo {
62 | pub class_name: String,
63 | pub style_string: String,
64 | }
65 |
--------------------------------------------------------------------------------
/styled_macro/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /Cargo.lock
3 |
--------------------------------------------------------------------------------
/styled_macro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "styled_macro"
3 | version = "0.1.1"
4 | edition = "2021"
5 | description = "Scoped styles for your Leptos components"
6 | license = "APL-1.0"
7 | keywords = ["leptos", "scoped", "styles", "styling", "CSS"]
8 | categories = ["wasm", "web-programming"]
9 | readme = "README.md"
10 | homepage = "https://github.com/eboody/styled"
11 |
12 | [lib]
13 | proc-macro = true
14 |
15 | [dependencies]
16 | proc-macro-error = "1.0.4"
17 | proc-macro2 = "1.0.51"
18 | quote = "1.0.23"
19 | stylist = "0.12.0"
20 | syn = "1.0.109"
21 | leptos = { git = "https://github.com/leptos-rs/leptos", default-features = false, features = [
22 | "serde",
23 | "ssr",
24 | "csr",
25 | "nightly"
26 | ], version = "0.4.8" }
27 | leptos_meta = { git = "https://github.com/leptos-rs/leptos", default-features = false, version = "0.4.8", features = ["nightly"] }
28 |
29 | [features]
30 | ssr = ["leptos/ssr", "leptos_meta/ssr"]
31 |
--------------------------------------------------------------------------------
/styled_macro/rust-toolchain:
--------------------------------------------------------------------------------
1 | nightly
2 |
--------------------------------------------------------------------------------
/styled_macro/src/lib.rs:
--------------------------------------------------------------------------------
1 | extern crate proc_macro;
2 | use proc_macro::TokenStream;
3 | use proc_macro2::TokenTree;
4 | use quote::quote;
5 |
6 | #[macro_use]
7 | extern crate proc_macro_error;
8 |
9 | #[proc_macro]
10 | pub fn view(tokens: TokenStream) -> TokenStream {
11 | let tokens: proc_macro2::TokenStream = tokens.into();
12 | let mut tokens = tokens.into_iter();
13 | tokens.next();
14 | let comma = tokens.next();
15 |
16 | match comma {
17 | Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => {
18 | let first = tokens.next();
19 | let second = tokens.next();
20 | let third = tokens.next();
21 | let fourth = tokens.next();
22 |
23 | let styles_result = match (&first, &second) {
24 | (Some(TokenTree::Ident(first)), Some(TokenTree::Punct(eq)))
25 | if *first == "styles" && eq.as_char() == '=' =>
26 | {
27 | match &fourth {
28 | Some(TokenTree::Punct(comma)) if comma.as_char() == ',' => third.clone(),
29 | _ => {
30 | abort!(
31 | punct, "To create scoped styles with the view! macro you must put a comma `,` after the value";
32 | help = r#"e.g., view!{cx, styles={my_styles_result}, ...
}"#
33 | )
34 | }
35 | }
36 | }
37 | _ => None,
38 | };
39 |
40 | let rest_of_tokens = tokens.collect::();
41 |
42 | let output = quote! {
43 | let hydration_context_id = leptos_dom::HydrationCtx::peek();
44 | let style_struct = #styles_result.unwrap();
45 | let class_name = format!("styled-{}", hydration_context_id);
46 | let style_string = style_struct.get_style_str().to_owned();
47 |
48 | style_struct.unregister();
49 |
50 | let mut style_string_with_fixed_pixels = String::new();
51 | let mut in_pixel_value = false;
52 |
53 | for (index, c) in style_string.chars().enumerate() {
54 | if !in_pixel_value && c == '-' {
55 | let char_one_over = &style_string.chars().nth(index - 1);
56 | let char_two_over = &style_string.chars().nth(index - 2);
57 | let char_three_over = &style_string.chars().nth(index - 3);
58 | if let (Some(char_one), Some(char_two), Some(char_three)) =
59 | (char_one_over, char_two_over, char_three_over)
60 | {
61 | if *char_one == 'x' && *char_two == 'p' && char_three.is_ascii_digit() {
62 | in_pixel_value = true;
63 | }
64 | }
65 | }
66 |
67 | if in_pixel_value {
68 | style_string_with_fixed_pixels.push(' ');
69 | in_pixel_value = false;
70 | }
71 |
72 | style_string_with_fixed_pixels.push(c);
73 | }
74 |
75 | let mut new_style_string = String::new();
76 | for line in style_string_with_fixed_pixels.lines() {
77 | if let Some(class_idx) = line.find(".stylist-") {
78 | if let Some(sel_idx) = line[class_idx..].find(|c: char| c.is_ascii_whitespace()) {
79 | let class = &line[class_idx..class_idx + sel_idx];
80 | let sel = match &line.chars().nth(&line.len() - 1) {
81 | Some('{') => &line[class_idx + sel_idx..line.len() - 1],
82 | _ => &line[class_idx + sel_idx..],
83 | };
84 |
85 | new_style_string.push('\n');
86 | new_style_string.push_str(sel.trim());
87 | new_style_string.push('.');
88 | new_style_string.push_str(&class_name);
89 | new_style_string.push_str(" {");
90 | }
91 | } else {
92 | new_style_string.push('\n');
93 | new_style_string.push_str(line);
94 | }
95 | }
96 | view! {
97 | cx,
98 | class={class_name.clone()},
99 |
100 | #rest_of_tokens
101 | }
102 | };
103 |
104 | output.into()
105 | }
106 | _ => {
107 | abort_call_site!(
108 | "view! macro needs a context and RSX: e.g., view! {{ cx, \
109 | ...
}}"
110 | )
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------