Completed Index post list.
And wrote a post about the framework and the Components.
This commit is contained in:
parent
dda65d1116
commit
fad0d06337
7 changed files with 257 additions and 14 deletions
|
@ -40,6 +40,7 @@ library
|
|||
Kit.Constants.Breakpoints
|
||||
Kit.Constants.Colors
|
||||
Kit.Constants.Spacing
|
||||
Kit.Molecules.Bricklayer
|
||||
Kit.Molecules.CardBody
|
||||
Kit.Molecules.CardFooter
|
||||
Kit.Molecules.CardHeader
|
||||
|
|
|
@ -9,6 +9,7 @@ This first post has two objectives:
|
|||
|
||||
* It is for me: a way to keep notes and to populate this site with real content.
|
||||
* It is for others: a resource to gather ideas and such.
|
||||
<!--more-->
|
||||
|
||||
# What?
|
||||
## A personal website
|
||||
|
@ -19,7 +20,6 @@ It'd be a website that compiles some personal posts. It should be
|
|||
* A photo gallery
|
||||
* Music?
|
||||
* A pattern guide / style guide
|
||||
<!--more-->
|
||||
|
||||
## A lightweight static site
|
||||
|
||||
|
@ -98,18 +98,17 @@ No hurry, I'd like to make it in under a year though...
|
|||
# Project launch
|
||||
Above was the first brief I made, and as I launch the project a few things have already changed.
|
||||
|
||||
* I will use Hamlet in place of Blaze, even though Blaze is used as Hamlet renders to the Html type defined in Blaze.
|
||||
* The idea of a Component data type that would smartly make a dependency graph of the different components used and use that information to generate a css file with only the actually used components seems to hard for now. It resorted to write my own css files.
|
||||
* ~~I will use Hamlet in place of Blaze, even though Blaze is used as Hamlet renders to the Html type defined in Blaze.~~
|
||||
* ~~The idea of a Component data type that would smartly make a dependency graph of the different components used and use that information to generate a css file with only the actually used components seems to hard for now. It resorted to write my own css files.~~
|
||||
|
||||
As of today I have the following file structure:
|
||||
```
|
||||
___src/___Core/... (Backend: compilers, routers, renderers and such)
|
||||
| |_Utils/... (utility function and structures)
|
||||
| |_Kit/___Atoms/... (Components: Hamlet + Clay)
|
||||
| | |_Molecules/...
|
||||
| | |_Organisms/...
|
||||
| | |_Templates/...
|
||||
| | |_Pages/...
|
||||
| |_Css/... (Styling: Clay css, import from kit and assemble)
|
||||
| |_Workshop/___Core/... (Some logic/styling to make a frontend workshop)
|
||||
| |_... (generate the frontend workshop)
|
||||
|_posts/... (YYYY-mm-dd-title.md blog posts)
|
||||
|
|
|
@ -5,11 +5,11 @@ date: 2022-10-27
|
|||
---
|
||||
|
||||
In this post I present the setup I use to keep notes of ideas, notions *etc*. It uses **vim** with a selected set of plugins, to achieve a minimalistic implementation of the [Zettelkasten](https://en.wikipedia.org/wiki/Zettelkasten) method.
|
||||
<!--more-->
|
||||
|
||||
# An overview of Zettelkasten
|
||||
|
||||
Behind this name lies a simple idea: instead of trying to classify notes in folders, or by tag and being limited by one particular classification, let the notes all live in a same large box and just be interested in the *metadata*: which notes link to which.
|
||||
<!--more-->
|
||||
|
||||
This way of keeping notes seems to be quite popular right now, and I must say I liked the idea that a note could just be dumped in a folder with just a few links to other notion to make it have its importance. I really lightens the burden of starting the process of writing something as it does not have to be classified yet, but will just classify itself in the process.
|
||||
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
title: Atomic design in Haskell using Hakyll, Blaze and Clay
|
||||
tags: web, haskell, hakyll, atomic-design
|
||||
description: An overview of the framework I use to make this site.
|
||||
date: 2023-06-20
|
||||
---
|
||||
|
||||
This post presents an overview of the mechanics I use to develop a pure Haskell framework that is atomic design compliant. I intend to write no `html` file, no `css` file and no `js` file, only `hs`. It uses [Hakyll](https://hackage.haskell.org/package/hakyll) to compile the site, [Blaze](https://hackage.haskell.org/package/blaze-html) to generate HTML and [Clay](https://hackage.haskell.org/package/clay) to generate CSS.
|
||||
<!--more-->
|
||||
|
||||
# Atomic design
|
||||
|
||||
As described by [Brad Frost](https://atomicdesign.bradfrost.com/table-of-contents/),
|
||||
|
||||
> Atomic design is a methodology composed of five distinct stages working together to create interface design systems in a more deliberate and hierarchical manner.
|
||||
|
||||
This methodology consists of designing:
|
||||
* **Atoms**: small elements such as buttons, media queries, containers, that only do *one* thing,
|
||||
* **Molecules**: bigger, more purposeful elements made of Atoms, *e.g.* a div with a button and some text,
|
||||
* **Organisms**: even bigger elements made of Atoms and Molecules, *e.g.* a card that displays a blog post with title, summary, date, link,
|
||||
* **Templates**: the biggest elements made of Atoms, Molecules and Organisms, *e.g.* a collection of cards per article for example.
|
||||
* **Pages**: instances of Templates with a given content.
|
||||
|
||||
The aim is to produce a scalable and cohesive system for a site.
|
||||
|
||||
I also like to add **Constants**, which are values such as colours, spacing, *etc.* They can change the site entirely, just as changing the value of fundamental constants would make a completely different Universe.
|
||||
|
||||
Of course, this hierarchy can be completed: I've seen the use of **Quarks** below Atoms, and you could have, say, **Ecosystems** on top of Organisms...
|
||||
|
||||
## The problem of CSS
|
||||
|
||||
Having this hierarchy is all well and good, but what about CSS? I really did not want to design a bunch of elements and having one big, messy `css` file aside which I'd have to fill every time. What if you change the name of a component? What if you designed an atomic button but finally don't use it in your site?
|
||||
|
||||
The solution I want is to be able to attach some CSS to a component in the same file as its definition, that would be carried around with the component. And when it is included in a bigger component, it would in some sense *inject* its CSS in its parent's.
|
||||
|
||||
# Presentation of the Haskell libraries
|
||||
|
||||
## Blaze
|
||||
|
||||
Blaze-html is a combinator library that allows to generate HTML. It defines an `Html` type which is a Monad (allowing the friendlier use of do notation). Typical use would be:
|
||||
|
||||
```haskell
|
||||
import Text.Blaze.Html5 as H
|
||||
import Text.Blaze.Html5.Attributes as A
|
||||
import Text.Blaze.Html.Renderer.Pretty
|
||||
|
||||
main :: IO ()
|
||||
main = putStr . renderHtml $ do
|
||||
ul ! class_ "foo" $ do
|
||||
li "bar"
|
||||
li "baz"
|
||||
```
|
||||
which when executed would give
|
||||
```
|
||||
<ul class="foo">
|
||||
<li>
|
||||
bar
|
||||
</li>
|
||||
<li>
|
||||
baz
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
## Clay
|
||||
|
||||
Clay does quite the same as Blaze, but with CSS. It defines a `Css` type just as Blaze does for HTML, and its use is very similar:
|
||||
|
||||
```haskell
|
||||
import Clay
|
||||
import Data.Text.Lazy.IO as T
|
||||
|
||||
main :: IO ()
|
||||
main = T.putStr . render $ ul # byClass "foo" ? do
|
||||
listStyle none outside none
|
||||
paddingLeft nil
|
||||
li ? do
|
||||
display inlineBlock
|
||||
```
|
||||
would yield
|
||||
```
|
||||
|
||||
ul.foo
|
||||
{
|
||||
list-style : none outside none;
|
||||
padding-left : 0;
|
||||
}
|
||||
|
||||
ul.foo li
|
||||
{
|
||||
display : inline-block;
|
||||
}
|
||||
|
||||
|
||||
/* Generated with Clay, http://fvisser.nl/clay */
|
||||
```
|
||||
|
||||
## Hakyll
|
||||
|
||||
Hakyll is a flat-file static site generator, *à la* Jekyll. It provides tools to craft compiling rules and takes care of dependencies. It also has templating mechanisms. I use the library for all the build logic, but in this post I want to highlight how I use it to compile the Haskell code I have. Say the two code snippets are called `html.hs` and `css.hs`.
|
||||
|
||||
In my project I use the `stack` package manager, but other means of compiling would work just as well:
|
||||
|
||||
```haskell
|
||||
import Hakyll
|
||||
|
||||
compileHs :: Compiler (Item String)
|
||||
compileHs = getResourceString
|
||||
>>= withItemBody (unixFilter "stack" ["runghc", "--", "-XOverloadedStrings"]
|
||||
|
||||
main :: IO ()
|
||||
main = hakyll $ do
|
||||
match "html.hs" $ do
|
||||
route $ constRoute "myUl.html"
|
||||
compile $ compileHs
|
||||
|
||||
match "css.hs" $ do
|
||||
route $ constRoute "css/default.css"
|
||||
compile $ compileHs
|
||||
```
|
||||
|
||||
# Creating components suited for atomic design
|
||||
|
||||
## Overview of the problem
|
||||
|
||||
Now that the libs are chosen and the details of compilation are off the table, let's get to the fun part: designing a `Component` type with the following properties:
|
||||
|
||||
* A `Component` has to carry around its styling *aka* Css.
|
||||
* It should be a polymorphic type: I'd like to have a `Component Html` but maybe sometimes a `Component (Html -> Html)` or who knows what.
|
||||
* It should be easy to assemble Components, *ie.* it should be an instance of Applicative (to nest Components) and Semigroup (to concatenate Components) at least.
|
||||
* Ideally, it should be extensible, *ie.* it should be possible to carry more than the Css around.
|
||||
* And now for the hard part: a Component will be used in several places in the code, but the corresponding Css must only be rendered *once*.
|
||||
|
||||
## Monoidal Props and the Writer Monad
|
||||
|
||||
After some trial and errors, I settled on using the [Writer Monad](https://hackage.haskell.org/package/mtl-2.2.2/docs/Control-Monad-Writer-Lazy.html) to achieve all of the above. To present it briefly, it is a Monad with an internal *accumulator*, instance of Monoid, such that every value (or *mobit*) can concatenate some data in the accumulator.
|
||||
|
||||
Now the Component type can be defined as
|
||||
```haskell
|
||||
type Component = Writer Prop
|
||||
```
|
||||
where Prop is a Monoid that contains Css and possibly other resources.
|
||||
|
||||
It's a slight change of perspective: instead of "carrying around" the Css, a Component just dumps it in the accumulator each time it is called. This type already takes care of the four first points, and some work must be done on Props to ensure the last point.
|
||||
|
||||
At the time of writing, here is what my Prop looks like:
|
||||
```haskell
|
||||
import Data.Map (Map, union, empty)
|
||||
|
||||
data Prop =
|
||||
{ cssMap :: Map String Css
|
||||
, assetsTree :: FileTree
|
||||
}
|
||||
|
||||
instance Semigroup Prop where
|
||||
Prop m t <> m' t' = Prop (union m m') (t <> t')
|
||||
|
||||
instance Monoid Prop where
|
||||
mempty = Prop empty mempty
|
||||
```
|
||||
Here, `FileTree` is a structure I defined to represent a tree of used assets that all live in the local folder, hence all have the same root. Such trees can thus be merged. I could also have used a `Set` with file names but I liked it less for some reason.
|
||||
|
||||
The choice of `Map` (a dictionary structure) is critical here: I give every component a name, such that if the key already exists in the accumulator, it is replaced but not duplicated. This is non ideal, as I'm the only guard against duplicate names, but it works. Another way could be to hash the Css for the key, but I don't really like the idea of rendering css, hashing it and pushing it to the Map only to then aggregate all Css and render it again.
|
||||
|
||||
## Two examples
|
||||
|
||||
So how do I use these Components? First, I make use of the Writer Monad `tell` function to declare some helpers such as:
|
||||
```haskell
|
||||
import Data.Map (singleton, empty)
|
||||
|
||||
addCss :: String -> Css -> Component ()
|
||||
addCss name css = tell
|
||||
$ Prop
|
||||
{ cssMap = singleton name css
|
||||
, assetsTree = mempty
|
||||
}
|
||||
|
||||
addAsset :: FilePath -> Component ()
|
||||
addAsset fp = tell
|
||||
$ Prop
|
||||
{ cssMap = empty
|
||||
, assetsTree = build fp -- build :: FilePath -> FileTree
|
||||
}
|
||||
```
|
||||
|
||||
And defining and combining Components is as easy as:
|
||||
```haskell
|
||||
css, css' :: Css
|
||||
css = ...
|
||||
|
||||
css' = ...
|
||||
|
||||
asset :: FilePath
|
||||
asset = "..."
|
||||
|
||||
comp :: Component (Html -> Html)
|
||||
comp = do
|
||||
addCss "comp" css
|
||||
addAsset asset
|
||||
return $ ... -- some Html -> Html function
|
||||
|
||||
comp' :: Component Html
|
||||
comp' = do
|
||||
addCss css'
|
||||
return $ ... -- some Html
|
||||
|
||||
comp'' :: Html
|
||||
comp'' = comp <*> (comp' <> comp')
|
||||
```
|
||||
|
||||
## Tackling atomic design
|
||||
|
||||
With `Component` I have everything I need to start defining Atoms, and combine them into Molecules and Organisms. For Templates I use a little trick: each of my templates has a `main` function that prints the Html value, and I make use of Hakyll's templating. I thus compile my Haskell code and it gives me a template to use in Hakyll. The final brick of the atomic hierarchy (Pages) are thus only defined through text file with metadata.
|
||||
|
||||
Now for the Css (and other resources): in my main file, I have a list with all Templates' `Component`, which I concatenate. This gives me a huge `Component` where every **used** Component (site-wise) has pushed its resources to the accumulator, and I can retrieve what I want. This is not final: what about assets that are called in a blog post? I will have to find a way to make them write to the accumulator as well.
|
||||
|
||||
But this is for another post...
|
|
@ -1,5 +1,6 @@
|
|||
module Kit.Atoms.BreakpointQueries
|
||||
( breakpointS
|
||||
( Query
|
||||
, breakpointS
|
||||
, breakpointM
|
||||
, breakpointL
|
||||
, breakpointXL
|
||||
|
@ -13,8 +14,9 @@ import Clay.Stylesheet ( Css
|
|||
|
||||
import Kit.Constants.Breakpoints
|
||||
|
||||
breakpointS, breakpointM, breakpointL, breakpointXL, breakpointXXL
|
||||
:: Css -> Css
|
||||
type Query = Css -> Css
|
||||
|
||||
breakpointS, breakpointM, breakpointL, breakpointXL, breakpointXXL :: Query
|
||||
breakpointS = query screen [minWidth s]
|
||||
breakpointM = query screen [minWidth m]
|
||||
breakpointL = query screen [minWidth l]
|
||||
|
|
25
src/Kit/Molecules/Bricklayer.hs
Normal file
25
src/Kit/Molecules/Bricklayer.hs
Normal file
|
@ -0,0 +1,25 @@
|
|||
module Kit.Molecules.Bricklayer where
|
||||
|
||||
import Clay as C
|
||||
import Components
|
||||
import Kit.Atoms.BreakpointQueries
|
||||
import Kit.Constants.Spacing as S
|
||||
import Text.Blaze.Html5 as H
|
||||
import Text.Blaze.Html5.Attributes as A
|
||||
|
||||
fr1 :: [Size LengthUnit]
|
||||
fr1 = Prelude.repeat (fr 1)
|
||||
|
||||
css :: Css
|
||||
css = element ".bricklayer" ? do
|
||||
C.width (pct 100)
|
||||
display grid
|
||||
gridGap S.large
|
||||
gridTemplateColumns (take 1 fr1)
|
||||
breakpointS $ gridTemplateColumns (take 2 fr1)
|
||||
breakpointL $ gridTemplateColumns (take 4 fr1)
|
||||
|
||||
bricklayer :: Component (Html -> Html)
|
||||
bricklayer = do
|
||||
addCss "Bricklayer" css
|
||||
return $ H.div H.! class_ "bricklayer"
|
|
@ -5,9 +5,8 @@ module Kit.Templates.Index
|
|||
|
||||
import Components
|
||||
import Core.Render
|
||||
import Kit.Atoms.Button
|
||||
import Kit.Atoms.ButtonLink
|
||||
import Kit.Atoms.Section
|
||||
import Kit.Molecules.Bricklayer
|
||||
import Kit.Organisms.Card
|
||||
import Kit.Organisms.Head
|
||||
import Kit.Organisms.ProfileHeader
|
||||
|
@ -17,7 +16,7 @@ import Text.Blaze.Html5 as H
|
|||
import Text.Blaze.Html5.Attributes as A
|
||||
|
||||
dummy :: PostProp
|
||||
dummy = PostProp { postRoute = Post "$url$"
|
||||
dummy = PostProp { postRoute = "$url$"
|
||||
, postTitle = "$title$"
|
||||
, postSummary = "$teaser$"
|
||||
, postDate = "$date$"
|
||||
|
@ -26,8 +25,8 @@ dummy = PostProp { postRoute = Post "$url$"
|
|||
indexBody :: Component Html
|
||||
indexBody =
|
||||
profileHeader
|
||||
<> ( section'
|
||||
<*> mconcat [pure "$for(pages)$", blogCard dummy, pure "$endfor$"]
|
||||
<> (section' <.> bricklayer <*> mconcat
|
||||
[pure "$for(pages)$", blogCard dummy, pure "$endfor$"]
|
||||
)
|
||||
|
||||
indexTemplate :: Component Html
|
||||
|
|
Loading…
Reference in a new issue