Open rebo opened 4 years ago
Here is an example app that demonstrates all of the above features:
https://github.com/rebo/seed_styling_app
Please note that you need to be on nightly (at least since 4th April 2020) due to new compiler features added recently.
Here is an update on the api for fully typed CSS (i.e. properties and values).
This file demonstrates creating a theme definition, including the named scales that are used in an app. This is done via simple enums:
#[derive(Hash,PartialEq, Eq, Clone)]
enum Color {
Primary,
DarkPrimary,
Secondary,
...
...
Once a theme is defined, specific values can create a theme instance:
AnyTheme::new()
.set(Color::Primary, CssColor::Hsl(200.0,70.0,80.0))
.set(Color::Secondary, hsl(300,60,50)) // or use the hsl shortcut
.set(Color::Highlight, hsl(310,70,85))
.set(Color::DarkPrimary, hsl(200,70,35))
...
...
As you can see above fully typed CssColor
can be used, alternatively a shortcut hsl()
can be used which creates this enum variant.
The typing is fairly extensive, so its often a good idea to use the helper functions:
.set(Size::Small, CssSize::Length(ExactLength{value:ordered_float::NotNan::new(4.0).unwrap(), unit:seed_hooks::style::measures::Unit::Px}))
vs
.set(Size::Medium, px(4).into()) // or use the px shortcut
Using a theme is as simple as passing the theme variant to the relevant css method:
button![
S.background_color(Color::Primary).color(Color::Secondary)
Of course &str
arguments can be used here as well. I.e. S.color("#FFF")
is available if you need it.
Full css method names can be a bit annoying, so shorter helper ones are available:
button![
S.bg_color(Color::Primary).color(Color::Secondary)
.radius(px(3))
.px(Space::Large)
.py(Space::Medium)
Theme scales can also be used although that is not demonstrated here.
If you have any suggestions for the api surface please give any suggestions.
Some notes based on some Discord discussions.
Currently the system is flexible in that all the below are possible.
Color properties: (i.e. background-color
, border-left-color
etc).
S.color(MyColors::Primary) // typical themed
S.color(Primary) // works if you `use MyColors::*`
S.color(CssColor::Hsl(40.,20.,80.)) // Typed with Hsl Variant
S.color(CssColor::Rgba(40.,20.,80.,0.3)) // Typed with Rgba Variant
S.color(CssColor::Hex(0xfff) // Typed with Hex Variant
S.color(hsl(40,20,80)) // Typed with hsl helper
S.color(rgba(40,20,30,0.2)) // Typed with rbga helper
S.color(rgb(40,20,30)) // Typed with rbg helper
S.color("#34542") // &str if people still want to use direct Strings.
For colors all the above seem reasonable.
For space properties I..e margin
padding-left
etc. We have:
S.padding(px(3)) // typed with px helper
S.padding(cm(2)) // typed with cm helper
S.padding(rem(1.4)) // typed with rem helper
// non-helper arguments can get verbose though
S.padding(CssPadding::Auto) // not too bad..
S.padding(CssPadding::Length(ExactLength{ value: 2.0, unit:Units::Px))) // now getting a bit annoying
Something like specifying a typed border
is particularly annoying due to needing
to specify width style and color. Technically width and color are optional however currently the enum needs all arguments specified. Therefore this is currently needed to fully specify a Css Border.
S.border(CssBorder::Border(BorderWidth::Length(ExactLength{value: "2.0", unit: Units::Px}),BorderStyle::Solid, BorderColor::Hsl(40.,30.,20.)))
no-one really has time for that. Especially just to produce border: 2px solid hsl(40,30,20);
You can't even use helper functions in the fully specified version because types have to match exactly. I.e. this wont compile:
S.border(CssBorder::Border(px(2),BorderStyle::Solid, hsl(40,30,20)) // still long and doesn't work
therefore we have a number of options.
0) Do nothing
1) many more global namespace helper functions beyond length (px
,cm
)and colors( rgb
, hsl
) This becomes difficult to remember and discover.
2) additional methods on S.
i.e.
S.border_width_style_color(
Some(px(3)),
CssBorderStyle::None,
Some(rgb(40,60,200))
) // still verbose and annoying for optional arguments.
These are discoverable through autocomplete on S.
but (many) hundreds of methods could slowdown autocomplete.
3) additional helper methods on each Css Type. i..e
S.border(CssBorder::solid_with_color_width(px(3),rgb(40,60,200)) // still ugly
4) accept a fn argument to act as a builder for the more annoying variants. I.e.
S.border(|v| v.solid().color(rbg(40,50,200)).px(2) )
S.border(|v| v.color(rbg(40,50,200)).px(2) ) // won't compile because a `style` is not present
Whist this is more terse, Im not feeling it 100% for any of these options really.
Maybe the solution is simply not use a composite property like CssBorder and instead use the constituent properties directly.
S.border_width(px(2))
.border_color(rgba(30,40,50))
.border_style(CssBorderStyle::None)
Not perfect but easier to scan.
In fact short helper methods for no argument variants. Make this even nicer:
S.border_width(px(2))
.border_color(MyColor::Primary))
.border_style_none()
So I think that is where it will go.
Currently Tailwind style shortcuts are included and work. i..e
S.text_red_300() // which translates to S.color(CssColor::hex(0xfeb2b2))
S.pl_2() // which translates to S.padding_left(rem(2))
Whilst these are fine as such, they massively increase the api surface which is already pretty big due to the various ways in which the styling system can be interacted with. Therefore probably going to move towards allowing shorter names but not using all the Tw Variants. Instead suggest scales either from a Theme or predefined. i..e
S.color(Colors::Red::n2) // Number 2 red from the theme.
S.pl(px(2)) // allow `pl()` as short version of padding-left.
Just an update prior to uploading an alpha of this.
Combinators are now implemented. i.e. S.is_child_of("p").font_weight_bold()
will produce:
p > [component_id] {
font-weight: bold;
}
and S.adjacent_follows("li").font_style_italic()
will produce
li + [component_id] {
font-style: italic;
}
Style properties accept slices of css values and are linked to the current themes' media query breakpoints. i.e.
S.padding( &[px(4), px(8), px(10)])
Will use 4px for the 1st breakpoint 8px for the next and 10px for the third.
This is a quick way of making styles responsive
Lastly Theme scale values can be referred to directly.
I.e for theme scale Theme::new.border_widths_scale([2px, 4px, 6px, 10px])
S.border_width(2)
will output
[component_id]{
border-width: 6px;
}
For more information on Theme scales see the theme specification : https://styled-system.com/theme-specification/ .
Another update, a Layout System is pretty much complete. With this you can create your layout declaratively. Then wire up specific views to typed areas once you have written them.
Define areas that might appear in a layout :
enum Area {
Header,
MainContent,
Nav,
SideBar,
Footer,
None,
}
Define the layout as a typed array:
use Area::*;
let mut layout = SeedLayout::new(&[
&[Header, Header, Header],
&[SideBar, MainContent, MainContent],
&[Footer, Footer, Footer],
]);
// and render
layout.render()
Produces this (note stub areas auto created based on enum variant):
If you start assigning Node<Msg>
views to areas:
layout.set(Header, red_background_view);
Then each area is filled with the respective view.
The layout system is fully responsive. Define a layout for small screens:
let mut sm_layout = SeedLayout::new(&[
&[Header],
&[Nav],
&[MainContent],
&[Footer]
]);
and compose with the larger layout keyed by theme breakpoints:
let mut comp = Composition::default();
comp.add(&[Breakpoint::LargeScreen], layout);
comp.add(&[Breakpoint::SmallScreen], sm_layout);
comp.render
and media queries & conditional rendering will automatically be setup to render this at lower screen sizes:
Nested layouts work fine. just comp.render()
or layout.render()
in a view function.
Media Query functionality does not stop there.
Conditionally render anything based on media query and theme breakpoints:
only(
Breakpoint::LargeScreen, ||
div![
S.w(px(80)).bg_color(Color::Primary),
"This div is rendered only on large Screens"
]
)
Alternatively you can render at that breakpoint and below (only_and_below
) or above (only_and_above
) or not at that breakpoint (except
).
Furthermore Css can be scoped to specific breakpoints by using the same methods but on a style object.
div![
S.only_and_below(Breakpoint::SmallScreen).color(rgb(255,0,0)),
This text is red on small screens
]
alternatively set properties for your theme breakpoint scale at once by passing an array to the repective property:
// in the theme
.breakpoint_scale([480, 800, 960]);
// set different shades and font sizes at the above breakpoints
S.background_color(&[
hsl(10, 80, 20), // from 0 to 479
hsl(10, 80, 50), // from 480 to 799
hsl(10, 80, 80), // from 800 to 959
hsl(10, 80, 90), // 960 and up etc...
])
.font_size(&[px(12) , px(14), px(18)])
Ok that is all for now! hope you like.
Ok that is all for now! hope you like.
awesome!
Outstanding!
This is next-level!
I spent some brain-time building this atomic SASS library and I think it might provide some different approaches to glance at as the element API becomes stable. In particular, it was the best way I could imagine implementing flexbox and worked really well in practice.
Wouldn't you just prefer to simply set styles...
...pretty clean!
This issue details a proposed styling system for Seed. It is currently implemented as described below in its own Seed App. I am starting a new issue and closing the old one ( #412 ) because significant changes and improvements have been made therefore it will be good to summarise the proposed api as it stands.
The goals of a Seed styling system are :
Existing solutions such as inline styles, use of existing frameworks, and .css file solutions are sub-optimum for reasons discussed in #412.
How it works
Property methods, such as
background_color()
create aStyle
object. On page render these are then injected into the<style>
tag in the page's<head>
. Styles are linked to the component that creates them by a unique class name. This enables the style to only affect that component.Here is the css that is inserted from the above code:
A comment is inserted to state where the style was originally defined.
Use of CSS
Many users will want to use CSS directly, perhaps from an online source or existing css files. This can be achieved with the
css()
method:Of course one can mix css with typed properties as well.
Named styles
Whilst the unique identifier and the CSS comment can help identify a component from a style a developer may prefer to also name a style to make that identification easier. This can be done with
name()
.generates the following css:
Convenience Properties
The ability to use functions to define properties mean that we can make use of shorter identifiers for more commonly used properties.
Ultra-convenience properties Tailwind style
We can go further and use Tailwind style shortcuts to greatly reduce the amount of typing for common styles. Furthermore due to these being fully validated there is no possibility of typos.
Standard Tailwind sizes and colours have been implemented
Pseudo classes
Pseudo classes are implemented with methods on the
Style
struct. For instance to mark aStyle
as hover simply callhover()
on the style.Overridable component styling
As a developer of a re-useable component you may wish to allow users to override the styles you have used.
Simply accept a
Style
argument and place it after the base style in the html element you want to render.This means you can follow a StyledSystem approach where all components can be styled at their call site.
Media Queries
Media queries are supported. Just call
media()
on the style and that block will be placed in the appropriate media query:Variant support
Support for variants is trivial, simply just add styles to a base style and give it a name.
The above button can therefore be rendered anywhere in your application as:
Theme Support
Theme Support is in place based on the Theme Specification. The user provides a theme object that provides size scales and identifies common colours.
This is achieved with the
Theme
type:To use this theme, the
use_theme()
function is used. Any component instantiated inside the use_theme block will have access to the theme.where inside
themed_button()
the theme is used as the below:Calling any of the property methods with a
( usize, &str)
tuple will use the corresponding scale value for that property or else the&str
default if the theme does not exist.Calling any of the property methods with a
( &str, &str)
tuple will use the corresponding aliased value for that property or else the default if the theme does not exist.The advantage of themes is that the entire colour scheme for a website can be adjusted in a single place (the theme object). For instance a
dark
orlight
theme might be provided.The interesting thing about themes under the Theme Specification is that the colour themes are available to any of the properties that set color. In the above example
background-color
was set to theprimary
theme color however settingborder-color
orcolor
would have worked just as well. Other properties are linked to the theme in similar ways, for instancespace
holds preset sizes for properties such aspadding
, `margin, etc.Keyframe support
Animation via keyframes is handled with the
keyframe()
method. This adds a style to a keyframes block at the specified key. This keyframe block is linked to the correct css class via a unique name set on theanimation-name
property. For instance:produces the following css:
Summary
As a whole the described style system achieves most of its goals. It can be used in a variety of ways familiar to users of a variety of CSS frameworks.
If someone can make any suggestions of niceties or even core functionality that you would like to include let me know.
Assuming users find the above code reasonable, concise and clean I will tidy up my code a little (i.e. it is all over the place right now) and release a sample app that can be played around with.