True Object Oriented CSS using C#
One of the reasons Foundation is such an affordable way to build bespoke software is our ability to abstract common logic into our libraries while still being able to 100% customize for each client as required. For our “under the hood” stuff, like data access and caching, we can use standard object-oriented mechanisms like interfaces & inheritance. Unfortunately for front-end work, very little has been contributed in terms of standards and best practice.
Front-end websites typically consist of three completely distinct, and yet completely interdependent languages - HTML, CSS & Javascript. This means additional languages for our developers to learn, and additional cost and inefficiency as we try to maintain different libraries and upgrades across projects.
In this article, I will share how Blackball has created an object-oriented framework for one of these components - CSS. By using C# to describe our CSS, we open up the full power of object-oriented programming, as well as a few extra bonus features.
TL;DR;
By the end of this article, your C# developers will be able to write CSS like this (full source code at end of article):
var backgroundImageSize = 50; var backgroundImageColor = #aaffaa; var backgroundImageBorderColor = #ccc"; this.position.Value = Positions.relative; this.minHeight.Value = backgroundImageSize; this.backgroundColor.Value = backgroundImageColor; var before = this.Before() .Icon(x => x.loading) .Set(x => x.backgroundColor, backgroundImageColor) .Set(x => x.textAlign, TextAlignments.left) .Set(x => x.fontSize, backgroundImageSize * 0.8) .Set(x => x.color, backgroundImageBorderColor) .Set(x => x.position, Positions.absolute) .Set(x => x.width, backgroundImageSize) .Set(x => x.height, backgroundImageSize) .Set(x => x.display, Display.block) .Set(x => x.marginLeft, -backgroundImageSize / 2) .Set(x => x.marginTop, -backgroundImageSize / 2); before.lineHeight.Pixels().Value = backgroundImageSize; before.left.Percentage().Value = 50; before.top.Percentage().Value = 50; before.boxShadow.Set(2, 2, 5, 5, backgroundImageBorderColor); before.borderRadius.Circle(); var an = before.Animate(); var ankf1 = an.KeyFrame(0); ankf1.Style.transform.RotateZ(0); var ankf2 = an.KeyFrame(100); ankf2.Style.transform.RotateZ(360);
Why OO CSS?
Compile-time checking
No external dependencies like gulp or webpack - just your regular C# compiler
Options to later generate different CSS based on browser types, screen size etc
Familiar environment and constructs for back-end developers
Ability to share and update styles between projects, while allowing them to remain 100% flexible
Features
Let’s get into the code. I’ve stripped things back to the basics here and will try to take you through progressively, adding more and more features.
The base class
First, we have to consider exactly what a base class is in CSS. It has to be self contained, it will probably have one or more properties. Here is the most basic example:
.list { background-color:red; width: 400px; }
In C#, this would be represented as:
public class List { public string BackgroundColor {get;set;} public string Width{get;set;} }
This is fine but it’s not re-usable. Consider another block of CSS that also included a height
property. We don’t want our developers having to create a C# class for every single block of CSS - there would be thousands, and it would be very slow. Instead, let me introduce our base class which I have simply called Css. This class simply has every single Css property available, all initially set to null
. When we come to render, we simply cherry-pick the non-null values:
public class Css{ public string SelectorName{get;set;} public enum SelectorType{get;set;} // All CSS properties... public string BackgroundColor {get;set;} public string Width{get;set;} public string Display{get;set;} public string VerticalAlign{get;set;} // ...etc etc }
At first it seems burdensome to have every CSS class listed, but in actual fact it isn’t. Remember also that the C# compiler strips out unused values so there is no performance overhead.
Note also my inclusion of the two top properties - SelectorName
and Selectortype
. Combined with a little bit of good old fashioned reflection, these classes may now render themselves like so:
public string CreateCss() { var s = new StringBuilder(); // Open the CSS s.AppendLine(this.SelectorType + this.SelectorName + "{"}); // Use reflection to iterate through our own properties foreach(PropertyInfo prop in this.GetType().GetProperties()){ var propertyName = prop.Name; // e.g. "width" var propertyValue = prop.GetValue(this); // e.g. "400px" s.AppendLine(propertyName + ": " + propertyValue + ";"); } // Close CSS block s.AppendLine("}"); return s.ToString(); }
Using this flexible class, you may now generate our list
CSS using the following C#:
var list = new Css(); list.SelectorName = "list"; list.SelectorType = "."; list.BackgroundColor = "red"; list.Width = "400px"; var css = list.CreateCss();
Property names
You may have noticed that the property names of my class (e.g. BackgroundColor
) do not confirm to the exact semantics of CSS (i.e. background-color
).. To address this, I have a simple attribute which I can apply to rogue class names:
/// <summary> /// Indicates the CSS class name that should be rendered in place of this property /// </summary> public class CssAttribute : Attribute { public string Name { get; private set; } public CssAttribute(string name) { this.Name = name; } }
Which can then be used to decorate properties and also in the CreateCss
method
[Css("background-color")] public string BackgroundColor{get;set;} // ... public string CreateCss(){ //.... foreach(PropertyInfo prop in this.GetType().GetProperties()){ // ... var propertyName = prop.Name; // Get specific render attributes var nameAttribute = prop.GetCustomAttribute<CssAttribute>(true); if (nameAttribute != null) { propertyName = nameAttribute.Name; } // ... etc } }
Property types
Another peculiarity about CSS is that each property can have a variety of different types. Consider the width
property for example, in CSS all of these are valid:
.list { width: 400px; width: 400.875676em; width: auto; width: inherit; width: calc(100% - 40px); width: 100em !important; }
Some of these are int
or double
values, but are in different units such as px
or em
. There are various keywords such as auto
or inherit
which from C#’s point of view can only be type string
. You can even have calculations and of course every property may have the !important
suffix.
To address these, and more, are architecture has the CssProperty<T>
class:
public class CssProperty<T>{ public T Value { get; set; } protected string Unit { get; set; } = ""; private bool IsImportant{get; set; } public CssProperty<T> Important() { this.IsImportant = true; return this; } public CssProperty<T> Pixels(){ this.Unit = "px"; return this; } public CssProperty<T> Percentage(){ this.Unit = "%"; return this; } public override string ToString(){ var result = this.Value.ToString() + this.Unit; if (this.IsImportant) result += " !important"; return result; } }
Using this, I can now set the Width
property of my base Css
class to type CssProperty<double>
, and in code set it as list.Width.Pixels().Value = 400
Note also the fluent interface which lets me chain commands together and makes it overall a little easier to read.
Calculations
The CssProperty
class also facilitates another powerful feature - calculations. Specifically, because my properties like width, height etc are doubles, I can add them together. This becomes very powerful later when we introduce variables.
Nested classes
All of the complexity above is required simply to allow you to render top-level CSS blocks. However, most CSS is actually targeting classes within classes, such as:
.list {width: 400px;} .list .item {display:block;}
To support this, our base Css
class has a new property called Children
, which is simply a collection of other Css
blocks:
public class Css{ private List<Css> Children { get; set; } = new List<ICss>(); // Other properties... public string CreateCss(){ var s = new StringBuilder(); // Render properties, as before // ... // Render each child foreach(var child in this.Children){ s.Append(child.CreateCss()); } return s.ToString(); }
Variations of the same technique can be used to render pseudo elements (e.g. :before
), media queries, and also concepts of mixins or extends that are offered by existing CSS preprocessors such as LESS or SCSS respectively.
Sharing CSS between projects
I said earlier that it was inefficient to create a new class for every style - hence the flexibility of the base Css
class above. However sometimes it is advantageous to separate a collection of styles out. Consider this class, which describes how a “clickable” item should look:
public class ClickableItem : Css, IClickableItem public IClickableItem Set(){ this.Mix<ITransition>().Set("opacity"); this.cursor.Value = Cursors.pointer; var hover = this.Hover(); hover.opacity.Value = 0.8; return this; } }
Of note:
the class inherits from our base
Css
class described aboveit sets its own properties
it implements an `IClickableItem` interface
To use it in code, a developer simply has to use the Mix
function:
var button = "input".CreateCss(); button.Mix<IClickableItem>().Set();
Because it is self-contained in a separate class, it may be mixed into any number of other properties throughout your site:
var mainMenu = "ul".CreateCss(); var li = mainMenu.Child("li"); li.display.Value = Display.InlineBlock; li.Mix<IClickableItem>().Set();
The cool part
Note in particular that the various code blocks are not referring to the ClickableItem
class directly - they are all routing through its IClickableItem
interface. This means that on any project, all you need to do is inject a new implementation via your IoC container and voila - all the code on your entire site is updated.
At Blackball, we include this interface and the default implementation in our base Foundation Nuget package, then override as required for specific projects.
The IClickableItem
is a trivial example - we have interfaces for tables, headings, loading feedback etc etc. It is extremely powerful and extremely flexible.
To conclude
There are plenty of ways to skin a cat. We are well aware of the various preprocessors, webpack plugins etc etc that are available to wrangle and re-use CSS. However we discarded these because it meant other dependencies in our code, and other skillsets to learn.
This does not mean however that our mechanism is any faster to develop on, or that the resulting CSS is any better. It just gives us a huge advantage when it comes to maintainability and re-usability across different projects.
The source code
Unfortunately it’s not practical for me to pull this code into a separate repo for you to download. However below I have cobbled together the key methods. It won’t compile, but it should be easy enough to work out if you are a C# developer. Have fun.
public class Css : ICss { #region Constructors and properties private readonly ICssBlock Block; private readonly IBreakpoints Breakpoints; private readonly IGlobalCssIcons Icons; /// <summary> /// Other random or new styles. Use this property if we have not accounted for the style with a specific property. Also good for vendor prefixes. /// </summary> [Css(false)] public Dictionary<string, object> Styles { get; set; } = new Dictionary<string, object>(); [Css(false)] private List<ICss> Children { get; set; } = new List<ICss>(); [Css(false)] private List<ICss> Parents { get; set; } = new List<ICss>(); [Css(false)] private List<ICss> ImmediateChildren { get; set; } = new List<ICss>(); [Css(false)] private List<ICss> States { get; set; } = new List<ICss>(); [Css(false)] private List<ICss> MediaQueries { get; set; } = new List<ICss>(); [Css(false)] private List<ICss> Mixins { get; set; } = new List<ICss>(); [Css(false)] public SelectorTypes SelectorType { get; set; } = SelectorTypes.Class; [Css(false)] public string Selector { get; set; } public Css() { } public Css(ICssBlock block, IBreakpoints breakpoints, IGlobalCssIcons icons) { Block = block; Breakpoints = breakpoints; Icons = icons; } #endregion #region CSS properties /// <summary> /// Allows us to set the member value and return self - so we can chain commands /// </summary> /// <typeparam name="TProperty"></typeparam> /// <param name="extract"></param> /// <param name="value"></param> /// <returns></returns> public ICss Set<TProperty>(Expression<Func<ICss, TProperty>> extract, double value) where TProperty : CssProperty<double?> { return this.Set(extract, (double?) value); } /// <summary> /// Allows us to set the member value and return self - so we can chain commands /// </summary> /// <typeparam name="TProperty"></typeparam> /// <param name="extract"></param> /// <param name="value"></param> /// <returns></returns> public ICss Set<TProperty>(Expression<Func<ICss, TProperty>> extract, int value) where TProperty : CssProperty<double?> { return this.Set(extract, (double?) value); } /// <summary> /// Allows us to set the member value and return self - so we can chain commands /// </summary> /// <typeparam name="TProperty"></typeparam> /// <typeparam name="TPropertyType"></typeparam> /// <param name="extract"></param> /// <param name="value"></param> /// <returns></returns> public ICss Set<TProperty, TPropertyType>(Expression<Func<ICss, TProperty>> extract, TPropertyType value) where TProperty : CssProperty<TPropertyType> { var memberExpression = (MemberExpression)extract.Body; var property = (PropertyInfo)memberExpression.Member; var cssProperty = property.GetValue(this) as CssProperty<TPropertyType>; if (cssProperty != null) cssProperty.Value = value; // property.SetValue(this, value); return this; } /// <inheritdoc /> public CssProperty<Positions> position { get; set; } = new CssProperty<Positions>(); public CssProperty<Display> display { get; set; } = new CssProperty<Display>(); [Css("font-weight")] public CssProperty<FontWeights> fontWeight { get; set; } = new CssProperty<FontWeights>(); [Css("text-align")] public CssProperty<TextAlignments> textAlign { get; set; } = new CssProperty<TextAlignments>(); public Pixel height { get; set; } = new Pixel(); public Flex flex { get; set; } = new Flex(); public Pixel width { get; set; } = new Pixel(); public CssProperty<Visibility> visibility { get; set; } = new CssProperty<Visibility>(); [Css("font-family")] public CssProperty<string> fontFamily { get; set; } = new CssProperty<string>(); public CssProperty<string> content { get; set; } = new CssProperty<string>(); public Transform transform { get; set; } = new Transform(); [Css("border-radius")] public BorderRadius borderRadius { get; set; } = new BorderRadius(); [Css("min-width")] public Pixel minWidth { get; set; } = new Pixel(); [Css("max-width")] public Pixel maxWidth { get; set; } = new Pixel(); [Css("min-height")] public Pixel minHeight { get; set; } = new Pixel(); [Css("max-height")] public Pixel maxHeight { get; set; } = new Pixel(); public Filter filter { get; set; } = new Filter(); [Css("font-size")] public Pixel fontSize { get; set; } = new Pixel(); [Css("line-height")] public Em lineHeight { get; set; } = new Em(); [Css("z-index")] public CssProperty<double?> zIndex { get; set; } = new CssProperty<double?>(); public CssProperty<double?> opacity { get; set; } = new CssProperty<double?>(); [Css("padding-right")] public Pixel paddingRight { get; set; } = new Pixel(); [Css("padding-top")] public Pixel paddingTop { get; set; } = new Pixel(); [Css("padding-bottom")] public Pixel paddingBottom { get; set; } = new Pixel(); [Css("padding-left")] public Pixel paddingLeft { get; set; } = new Pixel(); [Css("margin-right")] public Pixel marginRight { get; set; } = new Pixel(); [Css("margin-top")] public Pixel marginTop { get; set; } = new Pixel(); [Css("margin-bottom")] public Pixel marginBottom { get; set; } = new Pixel(); [Css("margin-left")] public Pixel marginLeft { get; set; } = new Pixel(); public CssProperty<double?> left { get; set; } = new CssProperty<double?>(); public CssProperty<double?> top { get; set; } = new CssProperty<double?>(); public CssProperty<double?> right { get; set; } = new CssProperty<double?>(); public CssProperty<double?> bottom { get; set; } = new CssProperty<double?>(); public CssProperty<Cursors> cursor {get; set; } = new CssProperty<Cursors>(); public TopRightBottomLeft padding {get; set; } = new TopRightBottomLeft(); public TopRightBottomLeft margin {get; set; } = new TopRightBottomLeft(); [Css("background-color")] public Color backgroundColor { get; set; } = new Color(); [Css("box-shadow")] public BoxShadow boxShadow { get; set; } = new BoxShadow(); public Color color { get; set; } = new Color(); public CssProperty<string> transition { get; set; } = new CssProperty<string>(); public Border border { get; set; } = new Border(); public Border borderBottom { get; set; } = new Border(); public Border borderTop { get; set; } = new Border(); public Border borderRight { get; set; } = new Border(); public Border borderLeft { get; set; } = new Border(); private Animation Animation { get; set; } = null; #endregion /// <summary> /// Adds the given mixin /// </summary> /// <param name="mixin"></param> public void Mix(ICss mixin) { this.Mixins.Add(mixin); } /// <summary> /// Instantiates an animation for this class /// </summary> /// <returns></returns> public Animation Animate() { var name = "kf_" + Guid.NewGuid().ToString().Replace("-", ""); this.Animation = new Animation() {Name = name}; return this.Animation; } public ICss Not(string selector, SelectorTypes type) { selector = $"not({this.FormatSelector(selector, type)})"; return this.Psuedo(selector); } public ICss Psuedo(string selector) { // Check if it exists? var existing = this.States.FirstOrDefault(x => x.Selector == selector && x.SelectorType == SelectorTypes.Psuedo); if (existing != null) return existing; var child = selector.CreateCss(SelectorTypes.Psuedo); this.States.Add(child); return child; } public ICss Icon<TProperty>(Expression<Func<IGlobalCssIcons, TProperty>> extract) { Icons.RenderIn(extract, this); return this; } /// <summary> /// Creates a :hover pseudo-element /// </summary> /// <returns></returns> public ICss Hover() { return Psuedo("hover"); } /// <summary> /// Creates a :focus pseudo-element /// </summary> /// <returns></returns> public ICss Focus() { return Psuedo("focus"); } /// <summary> /// Creates a media query block for our mobile breakpoint /// </summary> /// <returns></returns> public ICss Mobile() { return this.WhenNarrowerThan(this.Breakpoints.mobile); } /// <summary> /// Creates a media query block for our mobile breakpoint /// </summary> /// <returns></returns> public ICss NotMobile() { return this.WhenWiderThan(this.Breakpoints.mobile); } /// <summary> /// Creates a media query block for our mobile breakpoint /// </summary> /// <returns></returns> public ICss Tablet() { return this.WhenNarrowerThan(this.Breakpoints.tablet); } public ICss WhenNarrowerThan(double narrowerThan, string unit = "px") { return this.Media($@"only screen and (max-width: {narrowerThan}{unit})"); } public ICss WhenWiderThan(double widerThan, string unit = "px") { return this.Media($@"only screen and (min-width: {widerThan}{unit})"); } public ICss Media(string selector) { // Check if it exists? var existing = this.MediaQueries.FirstOrDefault(x => x.Selector == selector && x.SelectorType == SelectorTypes.Media); if (existing != null) return existing; var child = selector.CreateCss(SelectorTypes.Media); this.MediaQueries.Add(child); return child; } /// <summary> /// Creates a :before pseudo-element /// </summary> /// <returns></returns> public ICss Before() { return Psuedo("before"); } /// <summary> /// Creates a :after pseudo-element /// </summary> /// <returns></returns> public ICss After() { return Psuedo("after"); } /// <summary> /// Creates a child selector /// </summary> /// <returns></returns> public ICss Child(string childSelector, SelectorTypes type = SelectorTypes.Class) { // Check if it exists? var existing = this.Children.FirstOrDefault(x => x.Selector == childSelector && x.SelectorType == type); if (existing != null) return existing; var child = childSelector.CreateCss(type); this.Children.Add(child); return child; } /// <summary> /// Executes if this item is inside the given selector /// </summary> /// <returns></returns> public ICss Parent(string parentSelector, SelectorTypes type = SelectorTypes.Class) { // Check if it exists? var existing = this.Parents.FirstOrDefault(x => x.Selector == parentSelector && x.SelectorType == type); if (existing != null) return existing; var parent = parentSelector.CreateCss(type); this.Parents.Add(parent); return parent; } /// <summary> /// Creates a child selector /// </summary> /// <returns></returns> public ICss ImmediateChild(string childSelector, SelectorTypes type = SelectorTypes.Class) { // Check if it exists? var existing = this.ImmediateChildren.FirstOrDefault(x => x.Selector == childSelector && x.SelectorType == type); if (existing != null) return existing; var child = childSelector.CreateCss(type); this.ImmediateChildren.Add(child); return child; } /// <summary> /// Creates a new CSS block which only activates if the current CSS block *also* has this new selector /// </summary> /// <returns></returns> public ICss And(string additionalClassName, SelectorTypes type = SelectorTypes.Class) { // Check if it exists? var existing = this.States.FirstOrDefault(x => x.Selector == additionalClassName && x.SelectorType == type); if (existing != null) return existing; var child = additionalClassName.CreateCss(type); this.States.Add(child); return child; } /// <summary> /// Renders the CSS, including through any pre-processors /// </summary> /// <param name="omitSelector"></param> /// <returns></returns> public string Render(bool omitSelector = false) { var css = this.CreateCss(omitSelector); // Route through our preprocessor - it can take care of the mixin structure etc if (this.Block != null) { this.Block.Css = css; css = this.Block.Generate(); } return css; } /// <summary> /// Formats our selector depending on its type = combinator, attribute, classname etc /// </summary> /// <param name="selector"></param> /// <param name="type"></param> /// <returns></returns> private string FormatSelector(string selector, SelectorTypes type) { var fullSelector = ""; switch (type) { case SelectorTypes.Class: fullSelector = $@".{selector}";; break; case SelectorTypes.Psuedo: fullSelector = $@":{selector}"; break; case SelectorTypes.Attribute: fullSelector = $@"[{selector}]"; break; case SelectorTypes.ID: fullSelector = $@"#{selector}"; break; case SelectorTypes.Raw: case SelectorTypes.Element: fullSelector = $@"{selector}"; break; case SelectorTypes.Media: fullSelector = $@"@media {selector}"; break; default: throw new UserException($@"{nameof(SelectorTypes)}.{type.ToString()} is not supported by the {nameof(FormatSelector)} method"); } return fullSelector; } /// <summary> /// Returns the selector based on the selector identifier and type (e.g. attribute, class etc) /// </summary> /// <returns></returns> public string GetSelector() { return this.FormatSelector(this.Selector, this.SelectorType); } /// <summary> /// Creates the CSS based on the properties and children of this object /// </summary> /// <param name="omitSelector"></param> /// <param name="nestingLevel"></param> /// <returns></returns> public string CreateCss(bool omitSelector = false, int nestingLevel = 0) { var indent = new String('\t', nestingLevel); var result = new StringBuilder(); // If we don't have a selector, then we don't want to render it, obviously omitSelector = omitSelector || string.IsNullOrWhiteSpace(this.Selector); // Prefix with the selector if (!omitSelector) { var fullSelector = this.GetSelector(); result.AppendLine($@"{fullSelector} {{"); } // Render mixins first so that the caller has a chance to override the properties afterwards foreach (var mix in this.Mixins) { result.Append(indent + mix.CreateCss(true, nestingLevel)); } // States - ie. additional changes if the given class is also part of this item foreach (var state in this.States) { result.Append(indent + "\t&" + state.CreateCss(false, nestingLevel + 1)); } // Parents - classes which this item is INSIDE of foreach (var parent in this.Parents) { // .parent-selector & { // min-height:0px; // } result.AppendLine(indent + "\t" + parent.GetSelector() + " & {"); result.AppendLine("\t" + parent.CreateCss(true, nestingLevel + 1)); result.AppendLine(indent + "\t}"); } // Immediate children (using the > operator foreach (var immediateChild in this.ImmediateChildren) { result.Append(indent + "\t> " + immediateChild.CreateCss(false, nestingLevel + 1)); } // Get each property var type = this.GetType(); foreach (PropertyInfo prop in type.GetProperties()) { // Name var name = prop.Name; // Get specific render attributes var nameAttribute = prop.GetCustomAttribute<CssAttribute>(true); if (nameAttribute != null) { if (!nameAttribute.Render) continue; name = nameAttribute.Name; } // We only want the Css Properties if (!prop.PropertyType.IsInstanceOfGenericType(typeof(CssProperty<>))) continue; // The value type is the generic property in our CssProperty. For example, if we have CssProperty<double> then valueType will be double. If we // are using a Color, then valueType will resolve to string because Color : CssProperty<string> var valueType = prop.PropertyType.GetGenericBase(typeof(CssProperty<>)).GenericTypeArguments[0]; // Get the actual value of this property var value = prop.GetValue(this); if (value == null) continue; // Get the description of this property - "inline-block" or "auto !important" var valueDescription = value.ToString(); if (valueDescription == null) continue; // Is the *value* name overriden? This typical for enums, where C# does not allow us to write the enum values with proper css syntax (e.g. inline-block) var memInfo = valueType.GetMember(valueDescription); if (memInfo.Length > 0) { var attributes = memInfo[0].GetCustomAttributes(typeof(CssAttribute), false); if (attributes.Length > 0) { // Enum values na are/should be decorated with a non-render attribute if (!((CssAttribute)attributes[0]).Render) continue; // Set the property value valueDescription = ((CssAttribute)attributes[0]).Name; } } // Does this property have an extension? var valueUnit = ""; var unitInfo = prop.GetCustomAttribute<UnitAttribute>(true); if (unitInfo != null) valueUnit = unitInfo.Unit; var quote = prop.GetCustomAttribute<IncludeQuotesAttribute>() == null ? "" : "\""; result.AppendLine(indent + "\t" + $@"{name}:{quote}{valueDescription}{valueUnit}{quote};"); } // Other arbitrary classes foreach (var child in this.Styles) { if (child.Value == null) continue; result.AppendLine(indent + "\t" + child.Key + ":" + child.Value.ToString() + ";" ); } // Animation if (this.Animation != null) result.AppendLine(this.Animation.Render()); // Get each child foreach (var child in this.Children) { result.Append(indent + "\t" + child.CreateCss(false, nestingLevel + 1)); } // Media queries foreach (var child in this.MediaQueries) { result.Append(indent + "\t" + child.CreateCss(false, nestingLevel + 1)); } result.AppendLine(""); // Close class if (!omitSelector) result.AppendLine($@"{indent}}}"); return result.ToString(); } } public static class BuilderExtensions { private static readonly IBreakpoints Breakpoints = Dependency.Resolve<IBreakpoints>(); public static ICss CreateCss(this string className, SelectorTypes type = SelectorTypes.Class) { var css = Dependency.Resolve<ICss>(); css.Selector = className; css.SelectorType = type; return css; } public static T Mix<T>(this ICss css) where T : ICss { var mix = Dependency.Resolve<T>(); css.Mix(mix); return mix; } } public class Button : Css, IButton { private readonly IGlobalCssVariables Variables; public Button(ICssBlock block, IBreakpoints breakpoints, IGlobalCssIcons icons, IGlobalCssVariables variables) : base(block, breakpoints, icons) { Variables = variables; } public IButton Set(string backgroundColor, string fontColor, string borderColor) { this.display.Value = Display.inlineBlock; this.color.Value = fontColor; this.border.Set(1, backgroundColor, BorderTypes.solid); this.borderRadius.Value = this.Variables.inputBorderRadius; this.backgroundColor.Value = backgroundColor; this.padding.Set(0, this.Variables.gutter); this.height.Value = this.Variables.inputHeight; this.lineHeight.Pixels().Value = this.Variables.inputHeight; this.Mix<IClickableItem>().Set(); return this; } } public class ClickableItem : Css, IClickableItem { public IClickableItem Set() { this.Mix<ITransition>().Set("opacity"); this.cursor.Value = Cursors.pointer; var hover = this.Hover(); hover.opacity.Value = 0.8; return this; } } public interface IClickableItem : IMixin { IClickableItem Set(); } public class BoxShadow : CssProperty<string> { public BoxShadow() { } public BoxShadow Set(double offsetX = 5, double offsetY = 5, double blurRadius = 5, double spreadRadius = 5, string color = null) { if (color == null) color = Dependency.Resolve<IGlobalCssVariables>().shadowColor; // Comma separated if (!string.IsNullOrWhiteSpace(this.Value)) this.Value += ", "; this.Value = $"{offsetX}px {offsetY}px {blurRadius}px {spreadRadius}px {color}"; return this; } public BoxShadow Inset(double offsetX = 5, double offsetY = 5, double blurRadius = 5, double spreadRadius = 5, string color = null) { if (color == null) color = Dependency.Resolve<IGlobalCssVariables>().shadowColor; // Comma separated if (!string.IsNullOrWhiteSpace(this.Value)) this.Value += ", "; this.Value += $"inset {offsetX}px {offsetY}px {blurRadius}px {spreadRadius}px {color}"; return this; } } public class Color : CssProperty<string> { public Color Hex(string hexCode) { this.Value = hexCode.EnsureStart("#"); return this; } public Color RGB(double r, double g, double b, double a = 100) { this.Value = $@"rgba({r}, {g}, {b}, {a})"; return this; } } public class Pixel : CssProperty<double?> { public Pixel() { this.Pixels(); } } // Usage var root = Dependency.Resolve<ICss>(); var button = root.Child("button"); button.Mix<IButton>().Set("transparent", this.Variables.primaryButtonBackgroundColor, this.Variables.primaryButtonBackgroundColor); var primary = button.And("primary"); primary.Mix<IButton>().Set(this.Variables.primaryButtonBackgroundColor, this.Variables.primaryButtonTextColor, this.Variables.primaryButtonTextColor); root.Child("input[type=submit]", SelectorTypes.Element).Mix(primary);