@kbai/design-system
v0.3.2
Published
Typescript design system
Downloads
10,877
Readme
Design System
Currently, just a central place to keep my thoughts where I jot down current ideas for this project.
This design system extends past theming and consistent components. It implements a new layout conceptual model that transpiled into CSS. The purpose of this layout model is to be simple, easy to reason with, and easy to be autogenerated with logical rules. Additionally, it's purpose is to be able to powerfully generate declarative layouts that are completely fluid and responsive.
Concepts
In this design-system, the simplest building blocks are called a Box
. For clarity, in HTML, it is simply a div
.
In 99% of the user interfaces we build, we can break down the layout of our user interface down into a few simple relationships: position, size, and layout.
Position
Position refers to the positioning of a Box on the screen (the x, y coordinates). There should only exist 2 ways: absolute positioning and relative positioning. These are the same two CSS positioning properties.
Absolute positioning refers to specifying an x,y coordinate. The Box will be placed at that x,y coordinate regardless of where everything else is.
Relative positioning means that the Box will not control its own x,y coordinates. It will simply let its parent control where on the screen it is positioned.
CSS positioning properties such as fixed and sticky aren't quite needed. With the scroll model from this design-system, fixed and sticky properties can be done just relative and absolute positioning.
Layout
Every Box defines some sort of layout. The nice thing is that there are only two types of layout: Rows and Columns. Although seemingly over simplified, with some additional configuration options & nesting Boxes, rows and columns can powerfully express the layout of any user interface.
Size
Size is a tricky parameter. Although the only 2 properties of size are width and height, there are three different ways size can be calculated.
- Size can be calculated from its children's size.
- A Box can provide its own size
- Its parent can define the size of the child through layout
It gets complicated when the size is dependent on more than one of the above points. For example, a box which is going to let its children determine the size, but also has a maximum size that it can't go over is tricky to layout, especially when considering responsivity.
Concepts of Layout
Layout can be determined in a very functional way.
Width of the parent is our main parameter:
layout([ParentLayout, ParentPosition, width])
=> [width, Layout, Position]
=> eachChild([childLayout, Layout, Position, width])
=> [Layout, Position, Size, width]
For a Box, given a parameter width, it returns a tuple of [width, Layout, Position] which is then used to calculate
We can wrap this function above with higher order functions to allow our UI to be constrained with other parameters in addition to width. This allows for complete flexibility. However, we don't need this amount of flexibility or even necessarily want it. It would make our UI overly complex to reason with and it would make rendering the UI slow.
If we can define our user-interfaces with a set of functions, then we can generally say that layouts in our UIs are simply defined by piecewise linear functions. This means our properties are in the form of ax + b
, where a
is a multiplier, b
is a constant, and x
is the width (or the property it depends on). With more properties, we can represent the UI as a linear combination of x1
, x2
, etc.
Translating our concepts of Layout into Code
Since our layouts are simply piecewise functions, all we have representing our layout is many linear functions separated by if statements (breakpoints). In CSS they are handled with @media
queries. With CSS, we're limited to choosing a handful of media queries that act as our application breakpoints.
However, with our new model, we can powerfully express custom breakpoints for each child based on its parent's size.
We don't try and calculate these functions on the fly at run-time. Instead, we can interpret them into static CSS properties.
For style changes based on state, we can extract those functions and run them at runtime.
Basic Example
let boxLayout = cond(switch = size.width, values = {
=>[0 to 1200] = { layout = row }
=>[1200 to inf] = { layout = col, position = (5%, 10%) }
})
Box(spread = boxLayout, style = { fg = theme.background, bg = theme.white })
(children = [
RowItem(text = `Item`),
RowItem(text = `Item2`)
])
// output css for layout (use grid to layout rows and columns to take advantage of grid-gap)
.box {
display: grid;
grid-template-rows: 1fr;
grid-auto-flow: column;
grid-template-columns: auto;
min-width: 100px;
max-width: 500px;
width: 50%;
height: 80%;
}
@media only screen and (min-width: 1200px) {
.box {
display: grid;
grid-template-rows: auto;
grid-auto-flow: row;
grid-template-columns: 1fr;
position: absolute;
left: 5%;
top: 10%;
}
}
Conceptually, it's a functional language that compiles computes the layout of any user interface.
It's a tree representation of each component + a tree representation of each component's layout. Except, it takes advantage of powerful pattern matching for an expressive syntax.
It's like html or css where it only contains information about the structure, but it represents the data in a very powerful way.