├── .github └── workflows │ └── docs.yml ├── .gitignore ├── CNAME ├── Dockerfile ├── LICENSE ├── README.md ├── book ├── changelog.nim ├── config.nims ├── index.nim ├── tutorial.nim └── tutorial │ ├── basics.nim │ ├── config.nims │ ├── forms.nim │ └── layouts.nim ├── changelog.md ├── docker-compose.yml ├── karkas.nimble ├── nbook.nim ├── nimib.toml ├── src ├── karkas.nim └── karkas │ ├── styles.nim │ └── sugar.nim └── tutorial ├── config.nims ├── src ├── tutorial.nim └── tutorial │ ├── layout.nim │ ├── pages.nim │ ├── pages │ ├── forms.nim │ ├── index.nim │ └── notfound.nim │ ├── routes.nim │ └── state.nim └── tutorial.nimble /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | 8 | jobs: 9 | Book-and-Docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Book 15 | run: docker-compose run book 16 | 17 | - name: Docs 18 | run: docker-compose run docs 19 | 20 | - name: Deploy 21 | uses: peaceiris/actions-gh-pages@v3 22 | with: 23 | github_token: ${{ secrets.GITHUB_TOKEN }} 24 | publish_dir: ./docs 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.sublime-project 3 | *.sublime-workspace 4 | .idea 5 | 6 | *.exe 7 | *.html 8 | *.js 9 | *.idx 10 | htmldocs 11 | docs 12 | nbook 13 | 14 | nimcache/ 15 | testresults/ 16 | outputExpected.txt 17 | outputGotten.txt 18 | 19 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | karkas.nim.town 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nimlang/choosenim 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY . /usr/src/app 6 | 7 | RUN choosenim devel 8 | RUN nimble install -y 9 | RUN git config --global --add safe.directory /usr/src/app 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Constantine Molchanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Karkas 2 | 3 | **Karkas** is a library for [Karax](https://github.com/karaxnim/karax/) frontend framework that makes it a bit easier to build layouts and work with styles. 4 | 5 | Karkas is split into two modules: `karkas/styles` and `karkas/sugar`. 6 | 7 | `karkas/styles` contains ready to use styles to turn HTML elements into boxes and containers. 8 | 9 | `karkas/sugar` contains `<-` func that merges two styles and `k` func that turns a `string` into a `kstring`. Those can be used entirely on their own as syntactic sugar to Karax regardless of Karkas's styles. 10 | 11 | - [Homepage](https://karkas.nim.town) 12 | - [Tutorial](https://karkas.nim.town/tutorial.html) 13 | - [API Docs](https://karkas.nim.town/apidocs/theindex.html) 14 | - [Repo](https://github.com/moigagoo/karkas) 15 | 16 | 17 | # Installation 18 | 19 | Add Karkas to your .nimble file: 20 | 21 | ```nim 22 | requires karkas 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /book/changelog.nim: -------------------------------------------------------------------------------- 1 | import nimib, nimibook 2 | 3 | 4 | nbInit(theme = useNimibook) 5 | 6 | nbText: readFile("../changelog.md") 7 | 8 | nbSave 9 | 10 | -------------------------------------------------------------------------------- /book/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | switch("define", "normDebug") 3 | 4 | -------------------------------------------------------------------------------- /book/index.nim: -------------------------------------------------------------------------------- 1 | import nimib, nimibook 2 | 3 | 4 | nbInit(theme = useNimibook) 5 | 6 | nbText: readFile("../README.md") 7 | 8 | nbSave 9 | 10 | -------------------------------------------------------------------------------- /book/tutorial.nim: -------------------------------------------------------------------------------- 1 | import nimib, nimibook 2 | 3 | 4 | nbInit(theme = useNimibook) 5 | 6 | nbText: """ 7 | # Tutorial 8 | 9 | In this tutorial, we'll learn procs and syntax sugar provided by Karkas and build a layout with them. We'll start with basic rectangles, then play around with vertical and horizontal stacks, and build a form by composing those stacks. Finally, we'll create a layout that can show different content while preserving the basic page structure. 10 | 11 | See the complete code of the tutorial app in the Karkas repo: [https://github.com/moigagoo/karkas/tree/develop/tutorial](https://github.com/moigagoo/karkas/tree/develop/tutorial). 12 | 13 | 14 | # Prerequisites 15 | 16 | You'll need to install [Sauer](https://github.com/moigagoo/sauer) package to set up and compile the tutorial app: 17 | 18 | ```shell 19 | $ nimble install -y sauer 20 | ``` 21 | 22 | 23 | # Setup 24 | 25 | 1. Create a new Karax project: 26 | 27 | ```shell 28 | $ mkdir tutorial 29 | $ cd tutorial 30 | $ nimble init # create a "binary" package 31 | ... 32 | $ sauer init 33 | ``` 34 | 35 | 2. Open `src/tutorial/pages/index.nim` in your favorite editor. It should look like this: 36 | 37 | ```nim 38 | import karax/[karaxdsl, kdom, vdom] 39 | import kraut/context 40 | 41 | import ../pages 42 | import ../state 43 | 44 | 45 | proc render*(context: Context): VNode = 46 | currentPage = Page.index 47 | document.title = "index" 48 | 49 | buildHtml(tdiv): 50 | h1: text "index" 51 | ``` 52 | 53 | 3. Build the app: 54 | 55 | ```shell 56 | $ sauer make 57 | ``` 58 | 59 | 4. Run a local web server and open the app in your browser: 60 | 61 | ```shell 62 | $ sauer serve --browse 63 | ``` 64 | """ 65 | 66 | nbSave 67 | 68 | -------------------------------------------------------------------------------- /book/tutorial/basics.nim: -------------------------------------------------------------------------------- 1 | import nimib, nimibook 2 | 3 | 4 | nbInit(theme = useNimibook) 5 | 6 | nbText: """ 7 | # Basics 8 | 9 | ## Boxes 10 | 11 | **Box** is the most basic layout entity representing a rectangular block that can be put into containers. 12 | 13 | Turning a `div` into a box describes its relationships with its parent more than with its children. In other words, `box` style tells you nothing about how the items are arranged inside it but it tells you how this element behaves relative to its neighbors in the same container. 14 | 15 | Let's create a couple boxes to get familiar with them. 16 | 17 | 1. Import `karkas/styles` and create two boxes inside the root div by creating two `tdiv` nodes with `style = box()`: 18 | 19 | ```nim 20 | import karax/[karaxdsl, kdom, vdom] 21 | import kraut/context 22 | import karkas/styles 23 | 24 | import ../pages 25 | import ../state 26 | 27 | 28 | proc render*(context: Context): VNode = 29 | currentPage = Page.index 30 | document.title = "index" 31 | 32 | buildHtml(tdiv): 33 | h1: text "index" 34 | tdiv(style = box()): 35 | p: text "Box one" 36 | tdiv(style = box()): 37 | p: text "Box two" 38 | ``` 39 | 40 | 2. Build the app: 41 | 42 | ```shell 43 | $ sauer make 44 | ``` 45 | 46 | 3. You should see this in your browser at [http://localhost:1337/app.html#/](http://localhost:1337/app.html#/): 47 | """ 48 | 49 | nbKaraxCode: 50 | import karkas/styles 51 | 52 | karaxHtml: 53 | tdiv(style = box()): 54 | p: text "Box one" 55 | tdiv(style = box()): 56 | p: text "Box two" 57 | 58 | nbText: """ 59 | 4. Let's make the boxes visible bye adding borders around them by mixing in custom styles: 60 | 61 | ```nim 62 | import karax/[karaxdsl, kdom, vdom, kbase] 63 | import kraut/context 64 | import karkas/styles 65 | 66 | import ../pages 67 | import ../state 68 | 69 | 70 | proc render*(context: Context): VNode = 71 | currentPage = Page.index 72 | document.title = "index" 73 | 74 | buildHtml(tdiv): 75 | h1: text "index" 76 | tdiv(style = box().merge(style {border: kstring"solid"})): 77 | p: text "Box one" 78 | tdiv(style = box().merge(style {border: kstring"solid"})): 79 | p: text "Box two" 80 | 81 | ``` 82 | 83 | Rebuild the app with `sauer make` and refresh the browser page: 84 | """ 85 | 86 | nbKaraxCode: 87 | import karkas/styles 88 | 89 | karaxHtml: 90 | tdiv(style = box().merge(style {border: kstring"solid"})): 91 | p: text "Box one" 92 | tdiv(style = box().merge(style {border: kstring"solid"})): 93 | p: text "Box two" 94 | 95 | nbText: """ 96 | ## Stacks 97 | 98 | **Stack** is a rectangular container for boxes. There are two kinds of stacks: vertical and horizontal. 99 | 100 | Let's put our boxes into a stack and play with the way they fit inside it. 101 | 102 | 1. Add a new `tdiv` with style `hStack`: 103 | 104 | ```nim 105 | import karax/[karaxdsl, kdom, vdom, kbase] 106 | import kraut/context 107 | import karkas/styles 108 | 109 | import ../pages 110 | import ../state 111 | 112 | 113 | proc render*(context: Context): VNode = 114 | currentPage = Page.index 115 | document.title = "index" 116 | 117 | buildHtml(tdiv): 118 | h1: text "index" 119 | tdiv(style = hStack()): 120 | tdiv(style = box().merge(style {border: kstring"solid"})): 121 | p: text "Box one" 122 | tdiv(style = box().merge(style {border: kstring"solid"})): 123 | p: text "Box two" 124 | ``` 125 | 126 | Rebuild the app with `sauer make` and refresh the browser page: 127 | """ 128 | 129 | nbKaraxCode: 130 | import karkas/styles 131 | 132 | karaxHtml: 133 | tdiv(style = hStack()): 134 | tdiv(style = box().merge(style {border: kstring"solid"})): 135 | p: text "Box one" 136 | tdiv(style = box().merge(style {border: kstring"solid"})): 137 | p: text "Box two" 138 | 139 | nbText: """ 140 | 2. By default, a box takes as little space inside the stack as possible. To control how much space a box “wants” to have, set its `size` param: 141 | 142 | ```nim 143 | import karax/[karaxdsl, kdom, vdom, kbase] 144 | import kraut/context 145 | import karkas/styles 146 | 147 | import ../pages 148 | import ../state 149 | 150 | 151 | proc render*(context: Context): VNode = 152 | currentPage = Page.index 153 | document.title = "index" 154 | 155 | buildHtml(tdiv): 156 | h1: text "index" 157 | tdiv(style = hStack()): 158 | tdiv(style = box(size = 1).merge(style {border: kstring"solid"})): 159 | p: text "Box one" 160 | tdiv(style = box(size = 2).merge(style {border: kstring"solid"})): 161 | p: text "Box two" 162 | ``` 163 | 164 | Rebuild the app with `sauer make` and refresh the browser page: 165 | """ 166 | 167 | nbKaraxCode: 168 | import karkas/styles 169 | 170 | karaxHtml: 171 | tdiv(style = hStack()): 172 | tdiv(style = box(size = 1).merge(style {border: kstring"solid"})): 173 | p: text "Box one" 174 | tdiv(style = box(size = 2).merge(style {border: kstring"solid"})): 175 | p: text "Box two" 176 | 177 | nbText: """ 178 | By setting `size` values for the boxes inside a stack, you control the proportions of space distribution between the boxes. In the example above, we set the second box to be twice larger than the first one. 179 | 180 | 181 | ## Sugar 182 | 183 | Let's now use `karkas/sugar` to make our code less noisy and more pleasant to work with. 184 | 185 | We'll use `<-` func to merge styles. It has several useful properties compared to its built-in `merge` counterpart: 186 | 187 | 1. It automatically converts `string` to `kstring`. 188 | 2. It automatically applies `style` to the passed `(StyleAttr, kstring)` array. 189 | 190 | 191 | ```nim 192 | import karax/[karaxdsl, kdom, vdom] 193 | import kraut/context 194 | import karkas 195 | 196 | import ../pages 197 | import ../state 198 | 199 | 200 | proc render*(context: Context): VNode = 201 | currentPage = Page.index 202 | document.title = "index" 203 | 204 | buildHtml(tdiv): 205 | h1: text "index" 206 | tdiv(style = hStack()): 207 | tdiv(style = box(size = 1) <- {border: "solid"}): 208 | p: text "Box one" 209 | tdiv(style = box(size = 2) <- {border: "solid"}): 210 | p: text "Box two" 211 | ``` 212 | 213 | 214 | Rebuild the app with `sauer make` and refresh the browser page. 215 | 216 | The result is the same but the code is now more approachable: 217 | """ 218 | 219 | nbKaraxCode: 220 | import karkas 221 | 222 | karaxHtml: 223 | tdiv(style = hStack()): 224 | tdiv(style = box(size = 1) <- {border: "solid"}): 225 | p: text "Box one" 226 | tdiv(style = box(size = 2) <- {border: "solid"}): 227 | p: text "Box two" 228 | 229 | nbSave 230 | 231 | -------------------------------------------------------------------------------- /book/tutorial/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../../src") 2 | -------------------------------------------------------------------------------- /book/tutorial/forms.nim: -------------------------------------------------------------------------------- 1 | import nimib, nimibook 2 | 3 | 4 | nbInit(theme = useNimibook) 5 | 6 | nbText: """ 7 | # Forms 8 | 9 | Building web forms is one of basic tasks in web development. Let's build a form with Karkas by combining stacks and putting the form elements in boxes. 10 | 11 | 1. Create a new page in the tutorial app by running this command inside `tutorial` folder: 12 | 13 | ```shell 14 | $ sauer pages new forms --default 15 | ``` 16 | 17 | 2. Open `src/tutorial/pages/forms.nim` in your favorite editor and edit its content: 18 | 19 | ```nim 20 | import karax/[karaxdsl, kdom, vdom] 21 | import kraut/context 22 | import karkas 23 | 24 | import ../pages 25 | import ../state 26 | 27 | 28 | proc render*(context: Context): VNode = 29 | currentPage = Page.forms 30 | document.title = "forms" 31 | 32 | buildHtml(tdiv): 33 | h1: text "forms" 34 | 35 | tdiv(style = {width: "500px"}): 36 | tdiv(style = vStack()) 37 | ``` 38 | 39 | Our form is a vertical stack of fields, empty so far. This is why we're using `vStack()` style. 40 | 41 | **Note:** The outer fixed width `div` is there just to prevent the form from taking the entire page. 42 | 43 | 3. Now let's add a couple of inputs to the form: 44 | 45 | ```nim 46 | import karax/[karaxdsl, kdom, vdom] 47 | import kraut/context 48 | import karkas 49 | 50 | import ../pages 51 | import ../state 52 | 53 | 54 | proc render*(context: Context): VNode = 55 | currentPage = Page.forms 56 | document.title = "forms" 57 | 58 | buildHtml(tdiv): 59 | h1: text "forms" 60 | 61 | tdiv(style = {width: "500px"}): 62 | tdiv(style = vStack()): 63 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 64 | tdiv(style = box(2)): 65 | input(name = k"first_name", style = {width: k"100%"}) 66 | tdiv(style = box(1)): 67 | label(`for` = k"first_name"): 68 | text k"First name:" 69 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 70 | tdiv(style = box(2)): 71 | input(name = k"last_name", style = {width: k"100%"}) 72 | tdiv(style = box(1)): 73 | label(`for` = k"last_name"): 74 | text k"Last name:" 75 | ``` 76 | 77 | Each field is a horizontal stack of an input and its label. 78 | 79 | Note that we set `direction` param in `hStack` to `rightToLeft`. This makes it more natural to code our fields: instead of “label-input,” we describe a field as “input-label.” 80 | 81 | 4. Build the app with `sauer make` and open [http://localhost:1337/app.html#/forms](http://localhost:1337/app.html#/forms) in your browser. You should see something like this: 82 | """ 83 | 84 | nbKaraxCode: 85 | import karkas 86 | 87 | karaxHtml: 88 | tdiv(style = vStack()): 89 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 90 | tdiv(style = box(2)): 91 | input(name = k"first_name", style = {width: k"100%"}) 92 | tdiv(style = box(1)): 93 | label(`for` = k"first_name"): 94 | text k"First name:" 95 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 96 | tdiv(style = box(2)): 97 | input(name = k"last_name", style = {width: k"100%"}) 98 | tdiv(style = box(1)): 99 | label(`for` = k"last_name"): 100 | text k"Last name:" 101 | 102 | nbText: """ 103 | 5. To complete the example, let's add another field with a textarea and three radio buttons: 104 | 105 | ```nim 106 | import karax/[karaxdsl, kdom, vdom] 107 | import kraut/context 108 | import karkas 109 | 110 | import ../pages 111 | import ../state 112 | 113 | 114 | proc render*(context: Context): VNode = 115 | currentPage = Page.forms 116 | document.title = "forms" 117 | 118 | buildHtml(tdiv): 119 | h1: text "forms" 120 | 121 | tdiv(style = {width: "500px"}): 122 | tdiv(style = vStack()): 123 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 124 | tdiv(style = box(2)): 125 | input(name = k"first_name", style = {width: k"100%"}) 126 | tdiv(style = box(1)): 127 | label(`for` = k"first_name"): 128 | text k"First name:" 129 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 130 | tdiv(style = box(2)): 131 | input(name = k"last_name", style = {width: k"100%"}) 132 | tdiv(style = box(1)): 133 | label(`for` = k"last_name"): 134 | text k"Last name:" 135 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 136 | tdiv(style = box(2)): 137 | textarea(name = k"about_me", style = {width: k"100%"}) 138 | tdiv(style = box(1)): 139 | label(`for` = k"about_me"): 140 | text k"About me:" 141 | tdiv(style = hStack() <- {margin: "10px"}): 142 | tdiv(style = box(1)): 143 | input(`type` = k"radio", id = k"white", name = k"color", checked = true) 144 | label(`for` = k"white"): 145 | text k"White" 146 | tdiv(style = box(1)): 147 | input(`type` = k"radio", id = k"blue", name = k"color") 148 | label(`for` = k"blue"): 149 | text k"Blue" 150 | tdiv(style = box(1)): 151 | input(`type` = k"radio", id = k"red", name = k"color") 152 | label(`for` = k"red"): 153 | text k"Red" 154 | ``` 155 | 156 | Rebuild the app with `sauer make` and refresh the browser page: 157 | """ 158 | 159 | nbKaraxCode: 160 | import karkas 161 | 162 | karaxHtml: 163 | tdiv(style = vStack()): 164 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 165 | tdiv(style = box(2)): 166 | input(name = k"first_name", style = {width: k"100%"}) 167 | tdiv(style = box(1)): 168 | label(`for` = k"first_name"): 169 | text k"First name:" 170 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 171 | tdiv(style = box(2)): 172 | input(name = k"last_name", style = {width: k"100%"}) 173 | tdiv(style = box(1)): 174 | label(`for` = k"last_name"): 175 | text k"Last name:" 176 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 177 | tdiv(style = box(2)): 178 | textarea(name = k"about_me", style = {width: k"100%"}) 179 | tdiv(style = box(1)): 180 | label(`for` = k"about_me"): 181 | text k"About me:" 182 | tdiv(style = hStack() <- {margin: "10px"}): 183 | tdiv(style = box(1)): 184 | input(`type` = k"radio", id = k"white", name = k"color", checked = true) 185 | label(`for` = k"white"): 186 | text k"White" 187 | tdiv(style = box(1)): 188 | input(`type` = k"radio", id = k"blue", name = k"color") 189 | label(`for` = k"blue"): 190 | text k"Blue" 191 | tdiv(style = box(1)): 192 | input(`type` = k"radio", id = k"red", name = k"color") 193 | label(`for` = k"red"): 194 | text k"Red" 195 | 196 | nbSave 197 | 198 | -------------------------------------------------------------------------------- /book/tutorial/layouts.nim: -------------------------------------------------------------------------------- 1 | import nimib, nimibook 2 | 3 | 4 | nbInit(theme = useNimibook) 5 | 6 | nbText: """ 7 | # Layouts 8 | 9 | In the closing part of the tutorial, we'll create a layout to be used on all pages of the tutorial app. We'll build it as a separate module that is imported by individual pages. 10 | 11 | The layout will consist of a top navigation panel and the content block underneath it. The navigation panel will have basic logic preventing the user from going to the page they're already on. 12 | 13 | 1. In the tutorial app folder, in `src/tutorial`, create a file called `layout.nim` with the following content: 14 | 15 | ```nim 16 | import karax/[karaxdsl, kdom, vdom] 17 | import karkas 18 | 19 | import pages 20 | import state 21 | 22 | 23 | proc render*(body: VNode): VNode = 24 | buildHtml: 25 | tdiv: 26 | for node in body: 27 | node 28 | ``` 29 | 30 | 2. Now, do two things in `src/tutorial/pages/index.nim`, `src/tutorial/pages/forms.nim`, and `src/tutorial/pages/notfound.nim`: 31 | 32 | - Add `import ../layout` under `import ../state`. 33 | - Replace `buildHtml(tdiv)` with `layout.render buildHtml(tdiv) do`. 34 | 35 | For example, `src/tutorial/pages/index.nim` should look like this: 36 | 37 | ```nim 38 | import karax/[karaxdsl, kdom, vdom] 39 | import kraut/context 40 | import karkas 41 | 42 | import ../pages 43 | import ../state 44 | import ../layout 45 | 46 | 47 | proc render*(context: Context): VNode = 48 | currentPage = Page.index 49 | document.title = "index" 50 | 51 | layout.render buildHtml(tdiv) do: 52 | h1: text "index" 53 | tdiv(style = hStack()): 54 | tdiv(style = box(size = 1) <- {border: "solid"}): 55 | p: text "Box one" 56 | tdiv(style = box(size = 2) <- {border: "solid"}): 57 | p: text "Box two" 58 | ``` 59 | 60 | 3. Rebuild the app with `sauer make`, open it in your browser, and make sure that nothing changed. Except that now, we have factored out common logic to a shared module, which we can now edit and thus affect all pages at once. 61 | 62 | 4. Open `src/tutorial/layout.nim` in your favorite editor and update the code: 63 | 64 | ```nim 65 | import karax/[karaxdsl, kdom, vdom] 66 | import karkas 67 | 68 | import pages 69 | import state 70 | 71 | 72 | proc render*(body: VNode): VNode = 73 | buildHtml: 74 | tdiv(style = vStack()): 75 | tdiv(style = topPanel() <- hStack() <- {padding: "10px", boxShadow: "0 0 10px"}) 76 | tdiv: 77 | for node in body: 78 | node 79 | ``` 80 | 81 | We've set up basic structure for our layout: a vertical stack with a panel at the top and a content block at the bottom. 82 | 83 | Note how we describe the top panel. By composing `topPanel()` with `hStack()` and custom styles, we define a full-width horizontal stack container with padding and shadow. 84 | 85 | Rebuild the app with `sauer make` and check the new (now empty) panel: 86 | """ 87 | 88 | nbKaraxCode: 89 | import karkas 90 | 91 | proc layout*(body: VNode): VNode = 92 | buildHtml: 93 | tdiv(style = vStack()): 94 | tdiv(style = topPanel() <- hStack() <- {padding: "10px", boxShadow: "0 0 10px"}) 95 | tdiv: 96 | for node in body: 97 | node 98 | 99 | karaxHtml: 100 | layout buildHtml(tdiv) do: 101 | h1: text "index" 102 | tdiv(style = hStack()): 103 | tdiv(style = box(size = 1) <- {border: "solid"}): 104 | p: text "Box one" 105 | tdiv(style = box(size = 2) <- {border: "solid"}): 106 | p: text "Box two" 107 | 108 | nbText: """ 109 | 5. Now let's define a proc that generates a navigation entry and invoke it in the top panel. Based on the current page, it shows either a link to a page or just its name: 110 | 111 | ```nim 112 | import karax/[karaxdsl, kdom, vdom] 113 | import karkas 114 | 115 | import pages 116 | import state 117 | 118 | 119 | proc navEntry(page: Page, url, caption: string): VNode = 120 | buildHtml: 121 | tdiv(style = box() <- {minWidth: "50px"}): 122 | if state.currentPage != page: 123 | a(href = k url): 124 | text k caption 125 | else: 126 | text k caption 127 | 128 | proc render*(body: VNode): VNode = 129 | buildHtml: 130 | tdiv(style = vStack()): 131 | tdiv(style = topPanel() <- hStack() <- {padding: "10px", boxShadow: "0 0 10px"}): 132 | navEntry(index, "#/", "Index") 133 | navEntry(forms, "#/forms", "Forms") 134 | tdiv: 135 | for node in body: 136 | node 137 | ``` 138 | 139 | Rebuild the app with `sauer make` and see the complete app with navigation: 140 | """ 141 | 142 | nbKaraxCode: 143 | import karkas 144 | 145 | type Page = enum 146 | index 147 | forms 148 | notfound 149 | 150 | type State = object 151 | currentPage: Page 152 | 153 | var state: State 154 | 155 | 156 | proc navEntry(page: Page, url, caption: string): VNode = 157 | buildHtml: 158 | tdiv(style = box() <- {minWidth: "50px"}): 159 | if state.currentPage != page: 160 | a(href = k url): 161 | text k caption 162 | proc onclick = state.currentPage = page 163 | else: 164 | text k caption 165 | 166 | proc layout*(body: VNode): VNode = 167 | buildHtml: 168 | tdiv(style = vStack()): 169 | tdiv(style = topPanel() <- hStack() <- {padding: "10px", boxShadow: "0 0 10px"}): 170 | navEntry(index, "#/", "Index") 171 | navEntry(forms, "#/forms", "Forms") 172 | tdiv: 173 | for node in body: 174 | node 175 | 176 | karaxHtml: 177 | case state.currentPage 178 | of index: 179 | layout buildHtml(tdiv) do: 180 | h1: text "index" 181 | tdiv(style = hStack()): 182 | tdiv(style = box(size = 1) <- {border: "solid"}): 183 | p: text "Box one" 184 | tdiv(style = box(size = 2) <- {border: "solid"}): 185 | p: text "Box two" 186 | of forms: 187 | layout buildHtml(tdiv) do: 188 | h1: text "forms" 189 | tdiv(style = {width: "500px"}): 190 | tdiv(style = vStack()): 191 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 192 | tdiv(style = box(2)): 193 | input(name = k"first_name", style = {width: k"100%"}) 194 | tdiv(style = box(1)): 195 | label(`for` = k"first_name"): 196 | text k"First name:" 197 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 198 | tdiv(style = box(2)): 199 | input(name = k"last_name", style = {width: k"100%"}) 200 | tdiv(style = box(1)): 201 | label(`for` = k"last_name"): 202 | text k"Last name:" 203 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 204 | tdiv(style = box(2)): 205 | textarea(name = k"about_me", style = {width: k"100%"}) 206 | tdiv(style = box(1)): 207 | label(`for` = k"about_me"): 208 | text k"About me:" 209 | tdiv(style = hStack() <- {margin: "10px"}): 210 | tdiv(style = box(1)): 211 | input(`type` = k"radio", id = k"white", name = k"color", checked = true) 212 | label(`for` = k"white"): 213 | text k"White" 214 | tdiv(style = box(1)): 215 | input(`type` = k"radio", id = k"blue", name = k"color") 216 | label(`for` = k"blue"): 217 | text k"Blue" 218 | tdiv(style = box(1)): 219 | input(`type` = k"radio", id = k"red", name = k"color") 220 | label(`for` = k"red"): 221 | text k"Red" 222 | else: 223 | discard 224 | 225 | nbSave 226 | 227 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - [!]—backward incompatible change 4 | - [+]—new feature 5 | - [f]—bugfix 6 | - [r]—refactoring 7 | - [t]—test suite improvement 8 | - [d]—docs improvement 9 | 10 | 11 | ## 1.0.0 (August 2, 2023) 12 | 13 | - 🎉 Initial release. 14 | 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | docs: 5 | build: . 6 | image: karkas 7 | volumes: 8 | - .:/usr/src/app 9 | command: nimble docs 10 | 11 | book: 12 | build: . 13 | image: karkas 14 | volumes: 15 | - .:/usr/src/app 16 | command: nimble book 17 | 18 | -------------------------------------------------------------------------------- /karkas.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "1.0.0" 4 | author = "Constantine Molchanov" 5 | description = "Layout helpers and sugar for Karax" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 2.0.0" 13 | requires "karax >= 1.3.0" 14 | 15 | taskRequires "setupBook", "nimib >= 0.3.8", "nimibook >= 0.3.1" 16 | 17 | 18 | # Tasks 19 | 20 | task setupBook, "Compiles the nimibook CLI-binary used for generating the docs": 21 | exec "nim c -d:release nbook.nim" 22 | 23 | before book: 24 | rmDir "docs" 25 | exec "nimble setupBook" 26 | 27 | task book, "Generate book": 28 | exec "./nbook".toExe & " --mm:orc --deepcopy:on update" 29 | exec "./nbook".toExe & " --mm:orc --deepcopy:on build" 30 | 31 | after book: 32 | cpFile("CNAME", "docs/CNAME") 33 | 34 | before docs: 35 | rmDir "docs/apidocs" 36 | 37 | task docs, "Generate docs": 38 | exec "nimble doc --outdir:docs/apidocs --project --index:on src/karkas" 39 | 40 | -------------------------------------------------------------------------------- /nbook.nim: -------------------------------------------------------------------------------- 1 | import nimibook 2 | 3 | 4 | var book = initBookWithToc: 5 | entry("Welcome to Karkas!", "index.nim") 6 | section("Tutorial", "tutorial.nim"): 7 | entry("Basics", "tutorial/basics.nim") 8 | entry("Forms", "tutorial/forms.nim") 9 | entry("Layouts", "tutorial/layouts.nim") 10 | entry("Changelog", "changelog.nim") 11 | 12 | 13 | nimibookCli(book) 14 | 15 | -------------------------------------------------------------------------------- /nimib.toml: -------------------------------------------------------------------------------- 1 | [nimib] 2 | srcDir = "book" 3 | homeDir = "docs" 4 | 5 | [nimibook] 6 | title = "Karkas" 7 | description = "Layout helpers and sugar for Karax" 8 | git_repository_url = "https://github.com/moigagoo/karkas" 9 | 10 | -------------------------------------------------------------------------------- /src/karkas.nim: -------------------------------------------------------------------------------- 1 | import karkas/[styles, sugar] 2 | 3 | 4 | export styles, sugar 5 | 6 | -------------------------------------------------------------------------------- /src/karkas/styles.nim: -------------------------------------------------------------------------------- 1 | import karax/vstyles 2 | 3 | import sugar 4 | 5 | 6 | export vstyles 7 | 8 | 9 | type 10 | HDirection* = enum 11 | leftToRight, rightToLeft 12 | VDirection* = enum 13 | topToBottom, bottomToTop 14 | VPosition* = enum 15 | top, center, bottom 16 | HPosition* = enum 17 | left, center, right 18 | 19 | 20 | func box*(size = 0): VStyle = 21 | ## Basic rectangular element that can be put inside stacks. 22 | 23 | style {StyleAttr.flex: k $size} 24 | 25 | 26 | func hStack*(direction = HDirection.leftToRight): VStyle = 27 | ## Horizontal stack. Boxes inside it are stacked horizontally according to ``direction``. 28 | 29 | let 30 | defaultStyle = style {StyleAttr.display: k"flex"} 31 | directionStyle = case direction 32 | of HDirection.leftToRight: 33 | style {StyleAttr.flexDirection: k"row"} 34 | of HDirection.rightToLeft: 35 | style {StyleAttr.flexDirection: k"row-reverse"} 36 | 37 | defaultStyle <- directionStyle 38 | 39 | 40 | func vStack*(direction = VDirection.topToBottom): VStyle = 41 | ## Vertical stack. Boxes inside it are stacked vertically according to ``direction``. 42 | 43 | let 44 | defaultStyle = style {StyleAttr.display: k"flex"} 45 | directionStyle = case direction 46 | of VDirection.topToBottom: 47 | style {StyleAttr.flexDirection: k"column"} 48 | of VDirection.bottomToTop: 49 | style {StyleAttr.flexDirection: k"column-reverse"} 50 | 51 | defaultStyle <- directionStyle 52 | 53 | 54 | func topPanel*: VStyle = 55 | ## Full-width horizontal panel attached to the top of the page. 56 | 57 | style {StyleAttr.top: k"0", StyleAttr.width: k"100%"} 58 | 59 | 60 | func bottomPanel*: VStyle = 61 | ## Full-width horizontal panel attached to the bottom of the page. 62 | 63 | style {StyleAttr.bottom: k"0", StyleAttr.width: k"100%"} 64 | 65 | 66 | func sticky*: VStyle = 67 | ## Make a top or a bottom panel (or any other element) sticky, i.e. always visible regardless of page scroll. 68 | 69 | style {StyleAttr.position: k"sticky"} 70 | 71 | 72 | func vBar*: VStyle = 73 | ## Vertical bar that occupies the entire page height. Useful for sidebars. 74 | 75 | style {StyleAttr.height: k"100vh"} 76 | 77 | 78 | func fBox*(vPosition = VPosition.top, hPosition = HPosition.right): VStyle = 79 | ##[ Floating box. A container that can be positioned over other page elements in nine anchor points based on ``vPosition`` and ``hPosition``. 80 | 81 | Useful for notification stacks. 82 | ]## 83 | 84 | let 85 | defaultStyle = style {StyleAttr.position: k"fixed"} 86 | vPositionStyle = case vPosition 87 | of VPosition.top: 88 | style {StyleAttr.top: k"0"} 89 | of VPosition.center: 90 | style {StyleAttr.top: k"50%", StyleAttr.transform: k"translateY(-50%)"} 91 | of VPosition.bottom: 92 | style {StyleAttr.bottom: k"0"} 93 | hPositionStyle = case hPosition 94 | of HPosition.left: 95 | style {StyleAttr.left: k"0"} 96 | of HPosition.center: 97 | style {StyleAttr.left: k"50%", StyleAttr.transform: k"translateX(-50%)"} 98 | of HPosition.right: 99 | style {StyleAttr.right: k"0"} 100 | centerStyle = 101 | if vPosition == VPosition.center and hPosition == HPosition.center: 102 | style {StyleAttr.transform: k"translateY(-50%) translateX(-50%)"} 103 | else: 104 | style() 105 | 106 | defaultStyle <- vPositionStyle <- hPositionStyle <- centerStyle 107 | 108 | -------------------------------------------------------------------------------- /src/karkas/sugar.nim: -------------------------------------------------------------------------------- 1 | import karax/[kbase, vstyles] 2 | 3 | when defined(js): 4 | import karax/jdict 5 | 6 | 7 | export kbase, vstyles 8 | 9 | 10 | func k*(x: string): kstring = 11 | ## Convert string to kstring. 12 | 13 | kstring x 14 | 15 | 16 | func `<-`*(a, b: VStyle): VStyle = 17 | ## Merge style a with style b. 18 | 19 | a.merge(b) 20 | 21 | 22 | func `<-`*(a: VStyle, b: varargs[(StyleAttr, kstring)]): VStyle = 23 | ## Merge style a with style b where b is constructed on the fly from key-value pairs. 24 | 25 | a.merge(style b) 26 | 27 | 28 | when defined(js): 29 | func `<-`*(a: VStyle, b: varargs[(StyleAttr, string)]): VStyle = 30 | ## Merge style a with style b where b is constructed on the fly from key-value pairs and values are of type string. 31 | 32 | var kb: seq[(StyleAttr, kstring)] 33 | 34 | for (key, val) in b: 35 | kb.add (key, k(val)) 36 | 37 | a <- kb 38 | 39 | -------------------------------------------------------------------------------- /tutorial/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "../src") 2 | 3 | -------------------------------------------------------------------------------- /tutorial/src/tutorial.nim: -------------------------------------------------------------------------------- 1 | import karax/karax 2 | 3 | import kraut 4 | 5 | import tutorial/routes 6 | import tutorial/pages/notfound 7 | 8 | 9 | let renderer = routeRenderer(routes.routes, defaultRenderer = notfound.render) 10 | 11 | setRenderer(renderer) 12 | 13 | -------------------------------------------------------------------------------- /tutorial/src/tutorial/layout.nim: -------------------------------------------------------------------------------- 1 | import karax/[karaxdsl, kdom, vdom] 2 | import karkas 3 | 4 | import pages 5 | import state 6 | 7 | 8 | proc navEntry(page: Page, url, caption: string): VNode = 9 | buildHtml: 10 | tdiv(style = box() <- {minWidth: "50px"}): 11 | if state.currentPage != page: 12 | a(href = k url): 13 | text k caption 14 | else: 15 | text k caption 16 | 17 | proc render*(body: VNode): VNode = 18 | buildHtml: 19 | tdiv(style = vStack()): 20 | tdiv(style = topPanel() <- hStack() <- {padding: "10px", boxShadow: "0 0 10px"}): 21 | navEntry(index, "#/", "Index") 22 | navEntry(forms, "#/forms", "Forms") 23 | tdiv: 24 | for node in body: 25 | node 26 | 27 | -------------------------------------------------------------------------------- /tutorial/src/tutorial/pages.nim: -------------------------------------------------------------------------------- 1 | type Page* = enum 2 | notfound 3 | index 4 | forms -------------------------------------------------------------------------------- /tutorial/src/tutorial/pages/forms.nim: -------------------------------------------------------------------------------- 1 | import karax/[karaxdsl, kdom, vdom] 2 | import kraut/context 3 | import karkas 4 | 5 | import ../pages 6 | import ../state 7 | import ../layout 8 | 9 | 10 | proc render*(context: Context): VNode = 11 | currentPage = Page.forms 12 | document.title = "forms" 13 | 14 | layout.render buildHtml(tdiv) do: 15 | h1: text "forms" 16 | 17 | tdiv(style = {width: "500px"}): 18 | tdiv(style = vStack()): 19 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 20 | tdiv(style = box(2)): 21 | input(name = k"first_name", style = {width: k"100%"}) 22 | tdiv(style = box(1)): 23 | label(`for` = k"first_name"): 24 | text k"First name:" 25 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 26 | tdiv(style = box(2)): 27 | input(name = k"last_name", style = {width: k"100%"}) 28 | tdiv(style = box(1)): 29 | label(`for` = k"last_name"): 30 | text k"Last name:" 31 | tdiv(style = hStack(direction = rightToLeft) <- {margin: "10px"}): 32 | tdiv(style = box(2)): 33 | textarea(name = k"about_me", style = {width: k"100%"}) 34 | tdiv(style = box(1)): 35 | label(`for` = k"about_me"): 36 | text k"About me:" 37 | tdiv(style = hStack() <- {margin: "10px"}): 38 | tdiv(style = box(1)): 39 | input(`type` = k"radio", id = k"white", name = k"color", checked = true) 40 | label(`for` = k"white"): 41 | text k"White" 42 | tdiv(style = box(1)): 43 | input(`type` = k"radio", id = k"blue", name = k"color") 44 | label(`for` = k"blue"): 45 | text k"Blue" 46 | tdiv(style = box(1)): 47 | input(`type` = k"radio", id = k"red", name = k"color") 48 | label(`for` = k"red"): 49 | text k"Red" 50 | 51 | -------------------------------------------------------------------------------- /tutorial/src/tutorial/pages/index.nim: -------------------------------------------------------------------------------- 1 | import karax/[karaxdsl, kdom, vdom] 2 | import kraut/context 3 | import karkas 4 | 5 | import ../pages 6 | import ../state 7 | import ../layout 8 | 9 | 10 | proc render*(context: Context): VNode = 11 | currentPage = Page.index 12 | document.title = "index" 13 | 14 | layout.render buildHtml(tdiv) do: 15 | h1: text "index" 16 | tdiv(style = hStack()): 17 | tdiv(style = box(size = 1) <- {border: "solid"}): 18 | p: text "Box one" 19 | tdiv(style = box(size = 2) <- {border: "solid"}): 20 | p: text "Box two" 21 | 22 | -------------------------------------------------------------------------------- /tutorial/src/tutorial/pages/notfound.nim: -------------------------------------------------------------------------------- 1 | import karax/[karaxdsl, kdom, vdom] 2 | import kraut/context 3 | 4 | import ../pages 5 | import ../state 6 | import ../layout 7 | 8 | 9 | proc render*(context: Context): VNode = 10 | currentPage = Page.notfound 11 | document.title = "notfound" 12 | 13 | layout.render buildHtml(tdiv) do: 14 | h1: text "notfound" 15 | 16 | -------------------------------------------------------------------------------- /tutorial/src/tutorial/routes.nim: -------------------------------------------------------------------------------- 1 | import kraut/sugar 2 | 3 | 4 | routes: 5 | "/": index 6 | "#/forms/": forms -------------------------------------------------------------------------------- /tutorial/src/tutorial/state.nim: -------------------------------------------------------------------------------- 1 | import pages 2 | 3 | 4 | var currentPage*: Page 5 | 6 | -------------------------------------------------------------------------------- /tutorial/tutorial.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "Constantine Molchanov" 5 | description = "A new awesome nimble package" 6 | license = "MIT" 7 | srcDir = "src" 8 | bin = @["tutorial"] 9 | 10 | 11 | # Dependencies 12 | 13 | requires "nim >= 1.6.14" 14 | requires "karax >= 1.2.3", "kraut >= 1.0.3" 15 | 16 | taskRequires "serve", "static_server >= 2.2.1" 17 | 18 | 19 | # Tasks 20 | 21 | task make, "Build the app": 22 | exec findExe("karun") & " src/tutorial.nim" 23 | 24 | task serve, "Serve the app with a local server": 25 | echo "The app is served at: http://localhost:1337/app.html#/" 26 | echo() 27 | exec findExe("static_server") 28 | 29 | --------------------------------------------------------------------------------