├── .editorconfig ├── .github └── workflows │ ├── gh-pages.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── happyx_ui.nimble ├── screenshot.png ├── src ├── happyx_ui.nim └── happyx_ui │ ├── button.nim │ ├── card.nim │ ├── colors.nim │ ├── containers.nim │ ├── core.nim │ ├── events.nim │ ├── image.nim │ ├── input.nim │ ├── progress.nim │ ├── surface.nim │ ├── tag.nim │ ├── text.nim │ └── tooltip.nim └── tests ├── index.html ├── test.nim └── test.nim.cfg /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.nim] 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages 🌐 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths-ignore: 8 | - 'docs/**' 9 | - '.github/ISSUE_TEMPLATE/*' 10 | - '*.md' 11 | - '*.nimble' 12 | - '.gitignore' 13 | - 'LICENSE' 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | live_demo: 20 | runs-on: ubuntu-latest 21 | env: 22 | nim_version: '2.0.4' 23 | node_version: '20' 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Use Node.js ${{ env.node_version }} 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: ${{ env.node_version }} 31 | 32 | - name: Cache nimble 33 | id: cache-nimble 34 | uses: actions/cache@v4 35 | with: 36 | path: ~/.nimble 37 | key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} 38 | 39 | - uses: jiro4989/setup-nim-action@v1 40 | with: 41 | nim-version: ${{ env.nim_version }} 42 | 43 | - name: Install Dependencies 🔃 44 | run: | 45 | npm install uglify-js -g 46 | npm install --global pageres-cli 47 | nimble refresh 48 | nimble install happyx@#head -y 49 | 50 | - name: Build HappyX-UI Demo example 🌐 51 | timeout-minutes: 2 52 | run: | 53 | cd tests 54 | nim js -d:danger --opt:size --hints:off --warnings:off test 55 | uglifyjs test.js -c -m --mangle-props regex=/N[ST]I\w+/ -o test.js 56 | 57 | - name: Deploy documents 💨 58 | uses: peaceiris/actions-gh-pages@v4 59 | with: 60 | github_token: ${{ secrets.GITHUB_TOKEN }} 61 | publish_dir: ./tests 62 | if: github.ref == 'refs/heads/master' 63 | 64 | - name: Screenshot Website 65 | run: | 66 | rm rf -f screenshot.png 67 | pageres https://hapticx.github.io/happyx-ui/#/ --delay=5 --filename='screenshot' 68 | 69 | - name: Commit screenshot 🔨 70 | run: | 71 | ls 72 | git config --local user.email ${{ secrets.BOT_EMAIL }} 73 | git config --local user.name "github-actions [bot]" 74 | git add screenshot.png 75 | git commit -m "[bot] update screenshot.png" --quiet 76 | 77 | - uses: ad-m/github-push-action@master 78 | with: 79 | github_token: ${{ secrets.SECRET_TOKEN }} 80 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Testing 👨‍🔬 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'dev' 8 | paths-ignore: 9 | - 'docs/**' 10 | - '.github/ISSUE_TEMPLATE/*' 11 | - '*.md' 12 | - '*.nimble' 13 | - '.gitignore' 14 | - 'LICENSE' 15 | pull_request: 16 | paths-ignore: 17 | - 'docs/**' 18 | - '.github/ISSUE_TEMPLATE/*' 19 | - '*.md' 20 | - '*.nimble' 21 | - '.gitignore' 22 | - 'LICENSE' 23 | 24 | jobs: 25 | dependencies: 26 | name: Install dependencies 🧩 27 | runs-on: ${{ matrix.os }} 28 | strategy: 29 | matrix: 30 | os: 31 | - ubuntu-latest 32 | nim_version: 33 | - '2.0.0' 34 | - '2.0.4' 35 | env: 36 | TIMEOUT_EXIT_STATUS: 124 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: jiro4989/setup-nim-action@v1 40 | with: 41 | nim-version: ${{ matrix.nim_version }} 42 | - uses: actions/cache@v4 43 | with: 44 | path: | 45 | ~/.nimble 46 | ~/.choosenim 47 | key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} 48 | - name: Install Dependencies 🔃 49 | run: | 50 | nimble refresh 51 | nimble install -y -d 52 | js: 53 | name: Test with JavaScript 🧪 54 | needs: dependencies 55 | runs-on: ${{ matrix.os }} 56 | strategy: 57 | matrix: 58 | os: 59 | - ubuntu-latest 60 | env: 61 | TIMEOUT_EXIT_STATUS: 124 62 | steps: 63 | - uses: actions/checkout@v4 64 | - uses: actions/cache@v4 65 | with: 66 | path: | 67 | ~/.nimble 68 | ~/.choosenim 69 | key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} 70 | - name: Build JS tests 🍍 71 | run: | 72 | cd tests 73 | for file in $(ls -v testjs*.nim); do 74 | echo "###===--- JS Test for " $file " ---===###" 75 | /home/runner/.nimble/bin/nim js -d:debug --hints:off --warnings:off $file 76 | done 77 | shell: bash 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | .vs-code/ 4 | .vscode/ 5 | .vs/ 6 | 7 | # Cache 8 | nimcache/ 9 | nimble/ 10 | htmldocs/ 11 | 12 | # builded 13 | **/build/ 14 | happyx_ui_linkerArgs.txt 15 | 16 | # Logs 17 | *.log 18 | 19 | # Compiled 20 | *.js 21 | *.exe 22 | 23 | init.sh 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HapticX 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 |
2 | 3 | # HappyX-UI 4 | ### HappyX UI library 5 | 6 | [![Github Pages](https://img.shields.io/badge/LIVE_DEMO-100000?style=for-the-badge&logo=github&logoColor=f44336&labelColor=141d1f&color=f44336)](https://hapticx.github.io/happyx-ui/#/) 7 | 8 | 9 | [![wakatime](https://wakatime.com/badge/user/eaf11f95-5e2a-4b60-ae6a-38cd01ed317b/project/3d17a540-e4d1-458e-a335-fa8908dba246.svg?style=for-the-badge)](https://wakatime.com/badge/user/eaf11f95-5e2a-4b60-ae6a-38cd01ed317b/project/3d17a540-e4d1-458e-a335-fa8908dba246) 10 | 11 |
12 | 13 | This library was inspired by Jetpack Compose. It contains components that allow you to efficiently and quickly create an application. 14 | 15 | ## TODO 🏁 16 | ### Simple Components 17 | - [x] `Button`, `OutlineButton`; 18 | - [x] `Input`, `OutlineInput`, `Checkbox`; 19 | - [x] `Surface`; 20 | - [x] `Column`, `Row`; 21 | - [x] `Progress`; 22 | - [x] `Text`; 23 | - [x] `Image` (default + svg support); 24 | - [x] `Card`; 25 | - [x] `Box` (container with child position absolute); 26 | - [x] `Grid`; 27 | - [ ] `Slider` (range slider); 28 | - [x] `Stepper` (number changer); 29 | - [x] `Tag` (for content); 30 | - [x] `Tooltip`; 31 | - [x] `Switcher`; 32 | - [x] `ChildModifier` (applies modifier to all children (not recursive)); 33 | 34 | ### Complex Components 35 | - [ ] `Breadcrumb` (page navigation); 36 | - [ ] `Dropdown` (dropdown menu); 37 | - [ ] `BottomNavigation`; 38 | - [ ] `TabNavigation`; 39 | - [ ] `Pagination`; 40 | - [ ] `Tree`; 41 | 42 | 43 | 44 | ## Example 👀 45 | 46 | ![image](screenshot.png) 47 | 48 | -------------------------------------------------------------------------------- /happyx_ui.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | description = "UI library for HappyX web framework" 4 | author = "HapticX" 5 | version = "1.0.0" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | # Deps 10 | 11 | requires "nim >= 1.6.14" 12 | requires "happyx >= 3.10.2" 13 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HapticX/happyx-ui/0f9831edc98c830a92931f62d4074130e49b6dbc/screenshot.png -------------------------------------------------------------------------------- /src/happyx_ui.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx_ui/[ 3 | core, colors, containers, button, image, surface, 4 | text, card, input, progress, tooltip, tag 5 | ] 6 | 7 | 8 | export 9 | core, colors, 10 | containers, surface, 11 | button, image, text, card, input, progress, 12 | tooltip, tag 13 | -------------------------------------------------------------------------------- /src/happyx_ui/button.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./colors, 4 | ./core, 5 | ./events 6 | 7 | 8 | let style = buildHtml: 9 | tStyle: 10 | {fmt(""" 11 | button.hpx-button { 12 | position: relative; 13 | overflow: hidden; 14 | border-radius: 1rem; 15 | border: 0 solid transparent; 16 | cursor: pointer; 17 | background-color: ; 18 | padding: .5rem 1rem; 19 | outline: none; 20 | transition: all; 21 | transition-duration: .3s; 22 | color: ; 23 | font-weight: 900; 24 | font-size: 16px; 25 | } 26 | 27 | button.hpx-button:hover { 28 | background-color: ; 29 | } 30 | 31 | button.hpx-button:active { 32 | background-color: ; 33 | } 34 | 35 | span.hpx-rippleAnimation { 36 | width: 0; 37 | height: 0; 38 | border-radius: 50%; 39 | background: rgba(255, 255, 255, 0.5); 40 | transform: scale(0); 41 | position: absolute; 42 | opacity: 1; 43 | } 44 | .hpx-rippleAnimation { 45 | animation: ripple .5s ease; 46 | } 47 | 48 | @keyframes ripple { 49 | 100% { 50 | transform: scale(2); 51 | opacity: 0; 52 | } 53 | } 54 | 55 | button.hpx-outlined-button { 56 | border-radius: 1rem; 57 | border: .2rem solid ; 58 | cursor: pointer; 59 | background-color: transparent; 60 | padding: .3rem .8rem; 61 | outline: none; 62 | transition: all; 63 | transition-duration: .3s; 64 | color: ; 65 | font-weight: 700; 66 | font-size: 16px; 67 | } 68 | 69 | button.hpx-outlined-button:hover { 70 | border-color: ; 71 | color: ; 72 | } 73 | 74 | button.hpx-outlined-button:active { 75 | border-color: ; 76 | color: ; 77 | } 78 | """, '<', '>')} 79 | document.head.appendChild(style.children[0]) 80 | 81 | 82 | proc Button*(onClick: OnClick = defOnClick, modifier: Modifier = initModifier(), 83 | class: string = "", stmt: TagRef = nil): TagRef = 84 | buildHtml: 85 | tButton(style = modifier.build(), class = "hpx-button " & class): 86 | if not stmt.isNil: 87 | stmt 88 | @click: 89 | onClick() 90 | @mousedown: 91 | let posX = ev.target.offsetLeft 92 | let posY = ev.target.offsetTop 93 | var buttonWidth = ev.target.offsetWidth 94 | var buttonHeight = ev.target.offsetHeight 95 | let ripple = document.createElement("span") 96 | ev.target.appendChild(ripple) 97 | 98 | if buttonWidth >= buttonHeight: 99 | buttonHeight = buttonWidth 100 | else: 101 | buttonWidth = buttonHeight 102 | 103 | # Get the center of the element 104 | let x = (ev.MouseEvent.pageX - posX).float - buttonWidth / 2 105 | let y = (ev.MouseEvent.pageY - posY).float - buttonHeight / 2 106 | 107 | ripple.style.width = fmt"{buttonWidth}px" 108 | ripple.style.height = fmt"{buttonHeight}px" 109 | ripple.style.top = fmt"{y}px" 110 | ripple.style.left = fmt"{x}px" 111 | 112 | ripple.classList.add("hpx-rippleAnimation") 113 | @mouseup: 114 | let ripple = document.createElement("span") 115 | withVariables ev: 116 | withTimeout 250, t: 117 | clearTimeout(t) 118 | if not ripple.parentElement.isNil: 119 | ev.target.removeChild(ripple) 120 | 121 | proc OutlineButton*(onClick: OnClick = defOnClick, modifier: Modifier = initModifier(), 122 | class: string = "", stmt: TagRef = nil): TagRef = 123 | buildHtml: 124 | tButton(style = modifier.build(), class = "hpx-outlined-button " & class): 125 | if not stmt.isNil: 126 | stmt 127 | @click: 128 | onClick() 129 | -------------------------------------------------------------------------------- /src/happyx_ui/card.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core, 4 | ./colors 5 | 6 | 7 | let style = buildHtml: 8 | tStyle: 9 | {fmt(""" 10 | div.hpx-card { 11 | border-radius: 1.5rem; 12 | filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15)); 13 | background-color: ; 14 | transition: all; 15 | transition-duration: .3s; 16 | padding: 2rem; 17 | } 18 | """, '<', '>')} 19 | document.head.appendChild(style.children[0]) 20 | 21 | 22 | proc Card*(modifier: Modifier = initModifier(), class: string = "", stmt: TagRef): TagRef = 23 | buildHtml: 24 | tDiv(class = "hpx-card " & class, style = modifier.build()): 25 | stmt 26 | -------------------------------------------------------------------------------- /src/happyx_ui/colors.nim: -------------------------------------------------------------------------------- 1 | var 2 | PRIMARY_COLOR* = "#f44336" 3 | PRIMARY_HOVER_COLOR* = "#d32f2f" 4 | PRIMARY_ACTIVE_COLOR* = "#e57373" 5 | FOREGROUND_COLOR* = "#f1dcc1" 6 | FOREGROUND_HOVER_COLOR* = "#dfccb4" 7 | FOREGROUND_ACTIVE_COLOR* = "#d4c1ab" 8 | BACKGROUND_COLOR* = "#141d1f" 9 | BACKGROUND_HOVER_COLOR* = "#182225" 10 | BACKGROUND_ACTIVE_COLOR* = "#1c282b" 11 | -------------------------------------------------------------------------------- /src/happyx_ui/containers.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core 4 | 5 | 6 | proc Box*(horizontal: string = "start", vertical: string = "start", 7 | modifier: Modifier = initModifier(), class: string = "", stmt: TagRef = nil): TagRef = 8 | if not stmt.isNil: 9 | for i in stmt.childNodes: 10 | i.style.position = "absolute" 11 | let id = uid() 12 | buildHtml: 13 | tDiv(class = fmt"hpx-box-{id} {class}", style = modifier.build()): 14 | if not stmt.isNil: 15 | stmt 16 | tStyle: {fmt(""" 17 | div.hpx-box- { 18 | display: flex; 19 | position: relative; 20 | justify-content: ; 21 | align-items: ; 22 | } 23 | """, '<', '>')} 24 | 25 | 26 | proc Column*(spacing: string = "4px", horizontal: string = "start", vertical: string = "start", 27 | modifier: Modifier = initModifier(), class: string = "", stmt: TagRef = nil): TagRef = 28 | let id = uid() 29 | buildHtml: 30 | tDiv(class = fmt"hpx-column-{id} {class}", style = modifier.build()): 31 | if not stmt.isNil: 32 | stmt 33 | tStyle: {fmt(""" 34 | div.hpx-column- { 35 | display: flex; 36 | flex-direction: column; 37 | gap: ; 38 | justify-content: ; 39 | align-items: ; 40 | } 41 | """, '<', '>')} 42 | 43 | 44 | proc Row*(spacing: string = "4px", horizontal: string = "start", vertical: string = "start", 45 | modifier: Modifier = initModifier(), class: string = "", stmt: TagRef = nil): TagRef = 46 | let id = uid() 47 | buildHtml: 48 | tDiv(class = fmt"hpx-row-{id} {class}", style = modifier.build()): 49 | if not stmt.isNil: 50 | stmt 51 | tStyle: {fmt(""" 52 | div.hpx-row- { 53 | display: flex; 54 | gap: ; 55 | justify-content: ; 56 | align-items: ; 57 | } 58 | """, '<', '>')} 59 | 60 | 61 | proc Grid*(hSpacing: string = "4px", vSpacing: string = "4px", horizontal: string = "start", 62 | vertical: string = "start", cols: int = -1, rows: int = -1, 63 | modifier: Modifier = initModifier(), class: string = "", 64 | stmt: TagRef = nil): TagRef = 65 | let 66 | id = uid() 67 | colsCss = 68 | if cols != -1: 69 | fmt"grid-template-columns: repeat({cols}, minmax(0, 1fr));" 70 | else: 71 | "" 72 | rowsCss = 73 | if cols != -1: 74 | fmt"grid-template-rows: repeat({rows}, minmax(0, 1fr));" 75 | else: 76 | "" 77 | buildHtml: 78 | tDiv(class = fmt"hpx-grid-{id} {class}", style = modifier.build()): 79 | if not stmt.isNil: 80 | stmt 81 | tStyle: {fmt(""" 82 | div.hpx-grid- { 83 | display: grid; 84 | 85 | 86 | column-gap: ; 87 | row-gap: ; 88 | justify-content: ; 89 | align-items: ; 90 | } 91 | """, '<', '>')} 92 | 93 | 94 | proc ChildModifier*(modifier: Modifier, stmt: TagRef): TagRef = 95 | let style: cstring = cstring(modifier.build()) 96 | for i in stmt.childNodes: 97 | {.emit: "`i`.style.cssText += `style`".} 98 | stmt 99 | -------------------------------------------------------------------------------- /src/happyx_ui/core.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/strformat, 3 | std/strutils, 4 | std/random, 5 | happyx 6 | 7 | export strformat 8 | 9 | 10 | proc px*(val: SomeNumber): string = $val.float & "px" 11 | proc em*(val: SomeNumber): string = $val.float & "em" 12 | proc vw*(val: SomeNumber): string = $val.float & "vw" 13 | proc vh*(val: SomeNumber): string = $val.float & "vh" 14 | proc pt*(val: SomeNumber): string = $val.float & "pt" 15 | proc deg*(val: SomeNumber): string = $val.float & "deg" 16 | proc rad*(val: SomeNumber): string = $val.float & "rad" 17 | proc turn*(val: SomeNumber): string = $val.float & "turn" 18 | proc rem*(val: SomeNumber): string = $val.float & "rem" 19 | 20 | var uniqId = 0 21 | rendererHandlers.add(cast[tuple[key: string, p: AppEventHandler]]((key: "rendered", p: proc() = 22 | {.cast(gcsafe).}: 23 | uniqId = 0 24 | ))) 25 | proc uid*: string = 26 | result = "c" & $uniqId & $(uniqId+1) & $(uniqId+5) 27 | inc uniqId 28 | 29 | 30 | type 31 | Modifier* = ref object 32 | style*: seq[string] 33 | Cursor = enum 34 | Default = "cursor" 35 | Pointer = "pointer" 36 | Help = "help" 37 | ContextMenu = "context-menu" 38 | Progress = "progress" 39 | Wait = "wait" 40 | Cell = "cell" 41 | Crosshair = "crosshair" 42 | Text = "text" 43 | VerticalText = "vertical-text" 44 | Alias = "alias" 45 | Copy = "copy" 46 | Move = "move" 47 | NoDrop = "no-drop" 48 | NotAllowed = "not-allowed" 49 | Grab = "grab" 50 | Grabbing = "grabbing" 51 | AllScroll = "all-scroll" 52 | ColResize = "col-resize" 53 | RowResize = "row-resize" 54 | NResize = "n-resize" 55 | SResize = "s-resize" 56 | WResize = "w-resize" 57 | EResize = "e-resize" 58 | NeResize = "ne-resize" 59 | NwResize = "nw-resize" 60 | SeResize = "se-resize" 61 | SwResize = "sw-resize" 62 | EwResize = "ew-resize" 63 | NsResize = "ns-resize" 64 | NeswResize = "nesw-resize" 65 | NwseResize = "nwse-resize" 66 | ZoomIn = "zoom-in" 67 | ZoomOut = "zoom-out" 68 | DropShadow* = enum 69 | XM = "filter: drop-shadow(0 1px 1px rgb(0 0 0 / 0.05));" 70 | Default = "drop-shadow filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06));" 71 | MD = "filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));" 72 | LG = "filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1));" 73 | XL = "filter: drop-shadow(0 20px 13px rgb(0 0 0 / 0.03)) drop-shadow(0 8px 5px rgb(0 0 0 / 0.08));" 74 | XXL = "filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));" 75 | 76 | 77 | proc initModifier*: Modifier = 78 | Modifier(style: @[]) 79 | 80 | 81 | proc indexOf*(self: Modifier, key: string): int = 82 | for i in 0..self.style.high: 83 | if key & ":" in self.style[i]: 84 | return i 85 | return -1 86 | 87 | 88 | proc padding*(self: Modifier, left, top, right, bottom: string): Modifier = 89 | self.style.add(fmt"padding: {top} {right} {bottom} {left}") 90 | self 91 | proc padding*(self: Modifier, x, y: string): Modifier = 92 | self.style.add(fmt"padding: {y} {x}") 93 | self 94 | proc padding*(self: Modifier, value: string): Modifier = 95 | self.style.add(fmt"padding: {value}") 96 | self 97 | 98 | 99 | proc objectContain*(self: Modifier): Modifier = 100 | self.style.add(fmt"object-fit: contain") 101 | self 102 | proc objectCover*(self: Modifier): Modifier = 103 | self.style.add(fmt"object-fit: cover") 104 | self 105 | proc objectFill*(self: Modifier): Modifier = 106 | self.style.add(fmt"object-fit: fill") 107 | self 108 | proc objectNone*(self: Modifier): Modifier = 109 | self.style.add(fmt"object-fit: none") 110 | self 111 | proc objectScaleDown*(self: Modifier): Modifier = 112 | self.style.add(fmt"object-fit: scale-down") 113 | self 114 | 115 | 116 | proc objectBottom*(self: Modifier): Modifier = 117 | self.style.add(fmt"object-position: bottom") 118 | self 119 | proc objectCenter*(self: Modifier): Modifier = 120 | self.style.add(fmt"object-position: center") 121 | self 122 | proc objectLeft*(self: Modifier): Modifier = 123 | self.style.add(fmt"object-position: left") 124 | self 125 | proc objectLeftBottom*(self: Modifier): Modifier = 126 | self.style.add(fmt"object-position: left bottom") 127 | self 128 | proc objectLeftTop*(self: Modifier): Modifier = 129 | self.style.add(fmt"object-position: left top") 130 | self 131 | proc objectRight*(self: Modifier): Modifier = 132 | self.style.add(fmt"object-position: right") 133 | self 134 | proc objectRightBottom*(self: Modifier): Modifier = 135 | self.style.add(fmt"object-position: right bottom") 136 | self 137 | proc objectRightTop*(self: Modifier): Modifier = 138 | self.style.add(fmt"object-position: right top") 139 | self 140 | proc objectTop*(self: Modifier): Modifier = 141 | self.style.add(fmt"object-position: top") 142 | self 143 | 144 | 145 | proc margin*(self: Modifier, left, top, right, bottom: string): Modifier = 146 | self.style.add(fmt"margin: {top} {right} {bottom} {left}") 147 | self 148 | proc margin*(self: Modifier, x, y: string): Modifier = 149 | self.style.add(fmt"margin: {y} {x}") 150 | self 151 | proc margin*(self: Modifier, value: string): Modifier = 152 | self.style.add(fmt"margin: {value}") 153 | self 154 | 155 | 156 | proc width*(self: Modifier, value: string): Modifier = 157 | self.style.add(fmt"width: {value}") 158 | self 159 | proc height*(self: Modifier, value: string): Modifier = 160 | self.style.add(fmt"height: {value}") 161 | self 162 | 163 | 164 | proc minWidth*(self: Modifier, value: string): Modifier = 165 | self.style.add(fmt"min-width: {value}") 166 | self 167 | proc minHeight*(self: Modifier, value: string): Modifier = 168 | self.style.add(fmt"min-height: {value}") 169 | self 170 | 171 | 172 | proc maxWidth*(self: Modifier, value: string): Modifier = 173 | self.style.add(fmt"max-width: {value}") 174 | self 175 | proc maxHeight*(self: Modifier, value: string): Modifier = 176 | self.style.add(fmt"max-height: {value}") 177 | self 178 | 179 | 180 | proc rotate*(self: Modifier, x, y, z: bool, value: string): Modifier = 181 | let i = self.indexOf("transform") 182 | if i != -1: 183 | self.style[i] &= fmt" rotate({x.int} {y.int} {z.int} {value})" 184 | else: 185 | self.style.add(fmt"transform: rotate({x.int} {y.int} {z.int} {value})") 186 | self 187 | proc rotate*(self: Modifier, value: string): Modifier = 188 | let i = self.indexOf("transform") 189 | if i != -1: 190 | self.style[i] &= fmt" rotate({value})" 191 | else: 192 | self.style.add(fmt"transform: rotate({value})") 193 | self 194 | proc rotate3d*(self: Modifier, x, y, z: string): Modifier = 195 | let i = self.indexOf("transform") 196 | if i != -1: 197 | self.style[i] &= fmt" rotate3d({x} {y} {z})" 198 | else: 199 | self.style.add(fmt"transform: rotate3d({x} {y} {z})") 200 | self 201 | proc rotateX*(self: Modifier, value: string): Modifier = 202 | let i = self.indexOf("transform") 203 | if i != -1: 204 | self.style[i] &= fmt" rotateX({value})" 205 | else: 206 | self.style.add(fmt"transform: rotateX({value})") 207 | self 208 | proc rotateY*(self: Modifier, value: string): Modifier = 209 | let i = self.indexOf("transform") 210 | if i != -1: 211 | self.style[i] &= fmt" rotateY({value})" 212 | else: 213 | self.style.add(fmt"transform: rotateY({value})") 214 | self 215 | proc rotateZ*(self: Modifier, value: string): Modifier = 216 | let i = self.indexOf("transform") 217 | if i != -1: 218 | self.style[i] &= fmt" rotateY({value})" 219 | else: 220 | self.style.add(fmt"transform: rotateZ({value})") 221 | self 222 | 223 | 224 | proc translate*(self: Modifier, x, y: string): Modifier = 225 | let i = self.indexOf("transform") 226 | if i != -1: 227 | self.style[i] &= fmt" translate({x} {y})" 228 | else: 229 | self.style.add(fmt"transform: translate({x} {y})") 230 | self 231 | proc translate3d*(self: Modifier, x, y, z: string): Modifier = 232 | let i = self.indexOf("transform") 233 | if i != -1: 234 | self.style[i] &= fmt" translate3d({x} {y} {z})" 235 | else: 236 | self.style.add(fmt"transform: translate3d({x} {y} {z})") 237 | self 238 | proc translateX*(self: Modifier, value: string): Modifier = 239 | let i = self.indexOf("transform") 240 | if i != -1: 241 | self.style[i] &= fmt" translateX({value})" 242 | else: 243 | self.style.add(fmt"transform: translateX({value})") 244 | self 245 | proc translateY*(self: Modifier, value: string): Modifier = 246 | let i = self.indexOf("transform") 247 | if i != -1: 248 | self.style[i] &= fmt" translateY({value})" 249 | else: 250 | self.style.add(fmt"transform: translateY({value})") 251 | self 252 | proc translateZ*(self: Modifier, value: string): Modifier = 253 | let i = self.indexOf("transform") 254 | if i != -1: 255 | self.style[i] &= fmt" translateZ({value})" 256 | else: 257 | self.style.add(fmt"transform: translateZ({value})") 258 | self 259 | 260 | 261 | proc scale*(self: Modifier, x, y: string): Modifier = 262 | let i = self.indexOf("transform") 263 | if i != -1: 264 | self.style[i] &= fmt" scale({x} {y})" 265 | else: 266 | self.style.add(fmt"transform: scale({x} {y})") 267 | self 268 | proc scale3d*(self: Modifier, x, y, z: string): Modifier = 269 | let i = self.indexOf("transform") 270 | if i != -1: 271 | self.style[i] &= fmt" scale3d({x} {y} {z})" 272 | else: 273 | self.style.add(fmt"transform: scale3d({x} {y} {z})") 274 | self 275 | proc scaleX*(self: Modifier, value: string): Modifier = 276 | let i = self.indexOf("transform") 277 | if i != -1: 278 | self.style[i] &= fmt" scaleX({value})" 279 | else: 280 | self.style.add(fmt"transform: scaleX({value})") 281 | self 282 | proc scaleY*(self: Modifier, value: string): Modifier = 283 | let i = self.indexOf("transform") 284 | if i != -1: 285 | self.style[i] &= fmt" scaleY({value})" 286 | else: 287 | self.style.add(fmt"transform: scaleY({value})") 288 | self 289 | proc scaleZ*(self: Modifier, value: string): Modifier = 290 | let i = self.indexOf("transform") 291 | if i != -1: 292 | self.style[i] &= fmt" scaleZ({value})" 293 | else: 294 | self.style.add(fmt"transform: scaleZ({value})") 295 | self 296 | 297 | 298 | proc background*(self: Modifier, value: string): Modifier = 299 | self.style.add(fmt"background: {value}") 300 | self 301 | proc backgroundColor*(self: Modifier, value: string): Modifier = 302 | self.style.add(fmt"background-color: {value}") 303 | self 304 | 305 | 306 | proc borderRadius*(self: Modifier, value: string): Modifier = 307 | self.style.add(fmt"border-radius: {value}") 308 | self 309 | proc borderColor*(self: Modifier, value: string): Modifier = 310 | self.style.add(fmt"border-color: {value}") 311 | self 312 | proc borderWidth*(self: Modifier, value: string): Modifier = 313 | self.style.add(fmt"border-width: {value}") 314 | self 315 | proc borderStyle*(self: Modifier, value: string): Modifier = 316 | self.style.add(fmt"border-style: {value}") 317 | self 318 | 319 | 320 | proc textColor*(self: Modifier, value: string): Modifier = 321 | self.style.add(fmt"color: {value}") 322 | self 323 | proc fontSize*(self: Modifier, value: string): Modifier = 324 | self.style.add(fmt"font-size: {value}") 325 | self 326 | 327 | 328 | proc dropShadow*(self: Modifier, size: string, color: string = "#000000"): Modifier = 329 | self.style.add(fmt"filter: drop-shadow(0 0 {size} {color});") 330 | self 331 | proc dropShadow*(self: Modifier, size: DropShadow): Modifier = 332 | self.style.add($size) 333 | self 334 | 335 | 336 | proc cursor*(self: Modifier, cursor: string): Modifier = 337 | self.style.add(fmt"cursor: {cursor}") 338 | self 339 | proc cursor*(self: Modifier, cursor: Cursor): Modifier = 340 | self.style.add(fmt"cursor: {cursor}") 341 | self 342 | 343 | 344 | proc overflowHidden*(self: Modifier): Modifier = 345 | self.style.add("overflow: hidden") 346 | self 347 | 348 | proc style*(self: Modifier, s: string): Modifier = 349 | self.style.add(s) 350 | self 351 | 352 | 353 | # Positions 354 | proc absolute*(self: Modifier): Modifier = 355 | self.style.add("position: absolute") 356 | self 357 | proc relative*(self: Modifier): Modifier = 358 | self.style.add("position: relative") 359 | self 360 | proc fixed*(self: Modifier): Modifier = 361 | self.style.add("position: fixed") 362 | self 363 | proc sticky*(self: Modifier): Modifier = 364 | self.style.add("position: sticky") 365 | self 366 | 367 | 368 | proc colSpan*(self: Modifier, amount: int = 1): Modifier = 369 | self.style.add(fmt"grid-column: span {amount} / span {amount};") 370 | self 371 | proc rowSpan*(self: Modifier, amount: int = 1): Modifier = 372 | self.style.add(fmt"grid-row: span {amount} / span {amount};") 373 | self 374 | 375 | proc flowRow*(self: Modifier): Modifier = 376 | self.style.add("grid-auto-flow: row;") 377 | self 378 | proc flowCol*(self: Modifier): Modifier = 379 | self.style.add("grid-auto-flow: column;") 380 | self 381 | proc flowDense*(self: Modifier): Modifier = 382 | self.style.add("grid-auto-flow: dense;") 383 | self 384 | proc flowRowDense*(self: Modifier): Modifier = 385 | self.style.add("grid-auto-flow: row dense;") 386 | self 387 | proc flowColDense*(self: Modifier): Modifier = 388 | self.style.add("grid-auto-flow: column dense;") 389 | self 390 | 391 | 392 | proc flex*(self: Modifier): Modifier = 393 | self.style.add("display: flex") 394 | self 395 | proc inline*(self: Modifier): Modifier = 396 | self.style.add("display: inline") 397 | self 398 | proc inlineFlex*(self: Modifier): Modifier = 399 | self.style.add("display: inline-flex") 400 | self 401 | 402 | 403 | proc flexWrap*(self: Modifier): Modifier = 404 | self.style.add("flex-wrap: wrap") 405 | self 406 | proc flexWrapReverse*(self: Modifier): Modifier = 407 | self.style.add("flex-wrap: wrap-reverse") 408 | self 409 | proc flexNoWrap*(self: Modifier): Modifier = 410 | self.style.add("flex-wrap: nowrap") 411 | self 412 | 413 | 414 | proc build*(self: Modifier): string = 415 | self.style.join(";") 416 | proc build*(self: State[Modifier]): string = 417 | self.val.style.join(";") 418 | -------------------------------------------------------------------------------- /src/happyx_ui/events.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/dom 3 | 4 | type 5 | OnInput* = proc(text: string): void 6 | OnFocus* = proc(el: Element): void 7 | OnBlur* = proc(el: Element): void 8 | OnClick* = proc(): void 9 | OnToggle* = proc(value: bool): void 10 | OnSelect* = proc(value: int): void 11 | 12 | 13 | const 14 | defOnInput* = proc(text: string) = discard 15 | defOnFocus* = proc(el: Element) = discard 16 | defOnBlur* = proc(el: Element) = discard 17 | defOnToggle* = proc(value: bool) = discard 18 | defOnSelect* = proc(value: int) = discard 19 | defOnClick* = proc() = discard 20 | -------------------------------------------------------------------------------- /src/happyx_ui/image.nim: -------------------------------------------------------------------------------- 1 | import 2 | std/strutils, 3 | happyx, 4 | ./core 5 | 6 | 7 | proc Image*(source: string = "", alt: string = "", width: string = "auto", height: string = "auto", 8 | modifier: Modifier = initModifier(), class: string = ""): TagRef = 9 | buildHtml: 10 | if source.endsWith"svg": 11 | tObject( 12 | type = "image/svg+xml", 13 | style = modifier.build(), 14 | data = source, 15 | class = class, 16 | width = 17 | if width != "auto": 18 | width 19 | else: 20 | "", 21 | height = 22 | if height != "auto": 23 | height 24 | else: 25 | "", 26 | ) 27 | else: 28 | tImg( 29 | src = source, 30 | alt = alt, 31 | style = modifier.build(), 32 | class = "hpx-image " & class 33 | ) 34 | tStyle: {fmt(""" 35 | img.hpx-image { 36 | width: ; 37 | height: ; 38 | } 39 | """, '<', '>')} 40 | -------------------------------------------------------------------------------- /src/happyx_ui/input.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core, 4 | ./colors, 5 | ./events 6 | 7 | 8 | proc Input*(onInput: OnInput = defOnInput, onFocus: OnFocus = defOnFocus, 9 | state: State[string] = nil, id: string = "", hint: string = "", 10 | modifier: Modifier = initModifier(), class: string = "", 11 | stmt: TagRef = nil): TagRef = 12 | let i = uid() 13 | buildHtml: 14 | tDiv(class = fmt"hpx-input-container {class}"): 15 | tInput( 16 | class = fmt"hpx-input {class}", 17 | placeholder = " ", 18 | id = id, 19 | value = 20 | if state.isNil: 21 | "" 22 | else: 23 | state.val 24 | ): 25 | @input: 26 | onInput($ev.target.value) 27 | if not state.isNil: 28 | state.set($ev.target.value) 29 | if ($ev.target.value).len > 0: 30 | ev.target.Element.parentElement.classList.add("hpx-input-hint-container") 31 | else: 32 | ev.target.Element.parentElement.classList.remove("hpx-input-hint-container") 33 | @focus: 34 | onFocus(ev.target.Element) 35 | tLabel(class = fmt"hpx-input-placeholder"): 36 | if not stmt.isNil: 37 | stmt 38 | if hint.len > 0: 39 | tLabel(class = fmt"hpx-input-hint"): 40 | {hint} 41 | tStyle: {fmt(""" 42 | div.hpx-input-container { 43 | position: relative; 44 | display: flex; 45 | align-items: center; 46 | } 47 | input.hpx-input { 48 | border: 0; 49 | border-bottom-width: .15rem; 50 | border-bottom-color: ; 51 | border-bottom-style: solid; 52 | transition: all; 53 | transition-duration: .3s; 54 | outline: 0; 55 | background-color: transparent; 56 | color: ; 57 | font-size: 18px; 58 | padding: .1rem .5rem; 59 | } 60 | input.hpx-input:hover { 61 | border-bottom-color: ; 62 | } 63 | input.hpx-input:focus { 64 | border-bottom-color: ; 65 | } 66 | label.hpx-input-placeholder { 67 | position: absolute; 68 | transition: all; 69 | transition-duration: .3s; 70 | opacity: .7; 71 | pointer-events: none; 72 | font-size: 16px; 73 | padding: .1rem .5rem; 74 | } 75 | label.hpx-input-hint { 76 | position: absolute; 77 | transition: all; 78 | transition-duration: .3s; 79 | opacity: 0; 80 | pointer-events: none; 81 | font-size: 16px; 82 | padding: .1rem .5rem; 83 | } 84 | input#:focus + label.hpx-input-placeholder { 85 | transform: scale(75%) translateX(-15%) translateY(-150%); 86 | opacity: .9; 87 | } 88 | div.hpx-input-container:focus-within label.hpx-input-hint { 89 | opacity: .7; 90 | } 91 | div.hpx-input-hint-container:focus-within label.hpx-input-hint { 92 | opacity: 0; 93 | } 94 | input#:not(:placeholder-shown) + label.hpx-input-placeholder { 95 | transform: scale(75%) translateX(-15%) translateY(-150%); 96 | opacity: .9; 97 | } 98 | """, '<', '>')} 99 | 100 | 101 | proc OutlineInput*(onInput: OnInput = defOnInput, onFocus: OnFocus = defOnFocus, 102 | state: State[string] = nil, id: string = "", hint: string = "", 103 | modifier: Modifier = initModifier(), class: string = "", 104 | stmt: TagRef = nil): TagRef = 105 | let i = uid() 106 | buildHtml: 107 | tDiv(class = fmt"hpx-input-outline-container {class}"): 108 | tInput( 109 | class = fmt"hpx-input-outline {class}", 110 | placeholder = " ", 111 | id = id, 112 | value = 113 | if state.isNil: 114 | "" 115 | else: 116 | state.val 117 | ): 118 | @input: 119 | onInput($ev.target.value) 120 | if not state.isNil: 121 | state.set($ev.target.value) 122 | if ($ev.target.value).len > 0: 123 | ev.target.Element.parentElement.classList.add("hpx-input-outline-hint-container") 124 | else: 125 | ev.target.Element.parentElement.classList.remove("hpx-input-outline-hint-container") 126 | @focus: 127 | onFocus(ev.target.Element) 128 | tLabel(class = fmt"hpx-input-outline-placeholder"): 129 | if not stmt.isNil: 130 | stmt 131 | if hint.len > 0: 132 | tLabel(class = fmt"hpx-input-outline-hint"): 133 | {hint} 134 | tStyle: {fmt(""" 135 | div.hpx-input-outline-container { 136 | position: relative; 137 | display: flex; 138 | align-items: center; 139 | } 140 | input.hpx-input-outline { 141 | border: .15rem solid; 142 | transition: all; 143 | transition-duration: .3s; 144 | outline: 0; 145 | background-color: transparent; 146 | border-radius: .5rem; 147 | color: ; 148 | font-size: 18px; 149 | padding: .1rem .5rem; 150 | } 151 | input.hpx-input-outline:hover { 152 | border-color: ; 153 | } 154 | input.hpx-input-outline:focus { 155 | border-color: ; 156 | } 157 | label.hpx-input-outline-placeholder { 158 | position: absolute; 159 | transition: all; 160 | transition-duration: .3s; 161 | opacity: .7; 162 | pointer-events: none; 163 | font-size: 16px; 164 | padding: .1rem .5rem; 165 | } 166 | label.hpx-input-outline-hint { 167 | position: absolute; 168 | transition: all; 169 | transition-duration: .3s; 170 | opacity: 0; 171 | pointer-events: none; 172 | font-size: 16px; 173 | padding: .1rem .5rem; 174 | } 175 | input#:focus + label.hpx-input-outline-placeholder { 176 | transform: scale(75%) translateX(-15%) translateY(-150%); 177 | opacity: .9; 178 | } 179 | div.hpx-input-outline-container:focus-within label.hpx-input-outline-hint { 180 | opacity: .7; 181 | } 182 | div.hpx-input-outline-hint-container:focus-within label.hpx-input-outline-hint { 183 | opacity: 0; 184 | } 185 | input#:not(:placeholder-shown) + label.hpx-input-outline-placeholder { 186 | transform: scale(75%) translateX(-15%) translateY(-150%); 187 | opacity: .9; 188 | } 189 | """, '<', '>')} 190 | 191 | 192 | proc Checkbox*(onToggle: OnToggle = defOnToggle, state: State[bool] = nil, 193 | modifier: Modifier = initModifier(), class: string = "", 194 | id = "", stmt: TagRef): TagRef = 195 | let i = uid() 196 | buildHtml: 197 | tDiv(class = fmt"hpx-checkbox-container-{i} {class}"): 198 | if not state.isNil and state.val: 199 | tDiv(class = fmt"hpx-checkbox-on-{i}"): 200 | tSvg(width="16px", height="16px", fill="none", viewBox="0 0 24 24"): 201 | tPath(d="M4 12.6111L8.92308 17.5L20 6.5", stroke=BACKGROUND_COLOR, "stroke-linecap"="round", "stroke-linejoin"="round", "stroke-width"="2") 202 | else: 203 | tDiv(class = fmt"hpx-checkbox-off-{i}") 204 | if not stmt.isNil: 205 | tLabel(class = fmt"hpx-checkbox-label-{i}"): 206 | stmt 207 | @click: 208 | if not state.isNil and state.val: 209 | onToggle(false) 210 | if not state.isNil: 211 | state.set(false) 212 | else: 213 | onToggle(true) 214 | if not state.isNil: 215 | state.set(true) 216 | tStyle: {fmt(""" 217 | div.hpx-checkbox-container- { 218 | display: flex; 219 | gap: .3rem; 220 | align-items: center; 221 | cursor: pointer; 222 | } 223 | 224 | div.hpx-checkbox-off- { 225 | border: 3px solid; 226 | border-radius: .5rem; 227 | width: 16px; 228 | height: 16px; 229 | transition: all; 230 | transition-duration: .3s; 231 | } 232 | div.hpx-checkbox-container-:hover div.hpx-checkbox-off- { 233 | border-color: ; 234 | } 235 | div.hpx-checkbox-container-:active div.hpx-checkbox-off- { 236 | border-color: ; 237 | } 238 | 239 | div.hpx-checkbox-on- { 240 | border: 3px solid; 241 | background-color: ; 242 | border-radius: .5rem; 243 | width: 16px; 244 | height: 16px; 245 | transition: all; 246 | transition-duration: .3s; 247 | } 248 | div.hpx-checkbox-container-:hover div.hpx-checkbox-on- { 249 | background-color: ; 250 | border-color: ; 251 | } 252 | div.hpx-checkbox-container-:active div.hpx-checkbox-on- { 253 | background-color: ; 254 | border-color: ; 255 | } 256 | 257 | label.hpx-checkbox-label- { 258 | font-size: 18px; 259 | user-select: none; 260 | cursor: pointer; 261 | } 262 | """, '<', '>')} 263 | 264 | 265 | proc Switcher*(onToggle: OnToggle = defOnToggle, state: State[bool] = nil, 266 | modifier: Modifier = initModifier(), class: string = "", 267 | id = "", stmt: TagRef): TagRef = 268 | let 269 | i = uid() 270 | switchControlClass = 271 | if not state.isNil and state.val: 272 | fmt"hpx-switch-control-on-{i} hpx-switch-control-on-anim" 273 | else: 274 | fmt"hpx-switch-control-off-{i} hpx-switch-control-off-anim" 275 | switchClass = 276 | if not state.isNil and state.val: 277 | fmt"hpx-switch-on-{i}" 278 | else: 279 | fmt"hpx-switch-off-{i}" 280 | buildHtml: 281 | tDiv(class = fmt"hpx-switch-container-{i} {class}"): 282 | tDiv(class = fmt"hpx-switch-{i} {switchClass}"): 283 | tDiv(class = fmt"hpx-switch-control-{i} {switchControlClass}") 284 | if not stmt.isNil: 285 | tLabel(class = fmt"hpx-switch-label-{i}"): 286 | stmt 287 | @click: 288 | if not state.isNil and state.val: 289 | onToggle(false) 290 | if not state.isNil: 291 | state.set(false) 292 | else: 293 | onToggle(true) 294 | if not state.isNil: 295 | state.set(true) 296 | tStyle: {fmt(""" 297 | div.hpx-switch-container- { 298 | display: flex; 299 | gap: .3rem; 300 | align-items: center; 301 | cursor: pointer; 302 | } 303 | 304 | div.hpx-switch- { 305 | border-radius: 2rem; 306 | width: 48px; 307 | height: 24px; 308 | transition: all; 309 | transition-duration: .3s; 310 | position: relative; 311 | align-items: center; 312 | display: flex; 313 | transition: all; 314 | transition-duration: .3s; 315 | } 316 | 317 | div.hpx-switch-on- { 318 | background-color: ; 319 | } 320 | 321 | div.hpx-switch-off- { 322 | background-color: ; 323 | } 324 | 325 | div.hpx-switch-control- { 326 | border-radius: 1rem; 327 | position: absolute; 328 | transition: all; 329 | transition-duration: .3s; 330 | width: 16px; 331 | height: 16px; 332 | } 333 | 334 | div.hpx-switch-control-on- { 335 | right: 4px; 336 | background-color: ; 337 | } 338 | 339 | div.hpx-switch-control-off- { 340 | left: 4px; 341 | background-color: ; 342 | } 343 | 344 | label.hpx-switch-label- { 345 | font-size: 18px; 346 | user-select: none; 347 | cursor: pointer; 348 | } 349 | 350 | .hpx-switch-control-on-anim { 351 | animation: hpx-switch-control-off .3s; 352 | } 353 | 354 | .hpx-switch-control-off-anim { 355 | animation: hpx-switch-control-on .3s; 356 | } 357 | 358 | @keyframes hpx-switch-control-on { 359 | 0% { 360 | right: 4px; 361 | left: 28px; 362 | } 363 | 100% { 364 | right: 28px; 365 | left: 4px; 366 | } 367 | } 368 | 369 | @keyframes hpx-switch-control-off { 370 | 0% { 371 | right: 28px; 372 | left: 4px; 373 | } 374 | 100% { 375 | right: 4px; 376 | left: 28px; 377 | } 378 | } 379 | """, '<', '>')} 380 | 381 | 382 | proc Stepper*(modifier: Modifier = initModifier(), class: string = "", 383 | state: State[int] = nil, min: int = 0, max: int = 10, step: int = 1): TagRef = 384 | let i = uid() 385 | buildHtml: 386 | tDiv(class = "hpx-stepper-container-{i} {class}", style = modifier.build()): 387 | tDiv(class = "hpx-stepper-button-sub hpx-stepper-button"): 388 | "-" 389 | @click: 390 | if state.val - step >= min: 391 | state.set(state.val - step) 392 | tDiv(class = "current hpx-stepper-number-{i}"): 393 | {state.val} 394 | tDiv(class = "hpx-stepper-button-add hpx-stepper-button"): 395 | "+" 396 | @click: 397 | if state.val + step <= max: 398 | state.set(state.val + step) 399 | tStyle: {fmt(""" 400 | .hpx-stepper-container- { 401 | display: flex; 402 | justify-content: space-between; 403 | align-items: center; 404 | gap: .5rem; 405 | border-radius: .5rem; 406 | background-color: ; 407 | color: ; 408 | cursor: pointer; 409 | overflow: hidden; 410 | height: 1.5em; 411 | position: relative; 412 | } 413 | .hpx-stepper-button { 414 | font-weight: 700; 415 | user-select: none; 416 | width: 50%; 417 | height: 100%; 418 | display: flex; 419 | align-items: center; 420 | transition: all; 421 | transition-duration: .3s; 422 | background-color: ; 423 | } 424 | .hpx-stepper-button:hover { 425 | background-color: ; 426 | } 427 | .hpx-stepper-button:active { 428 | background-color: ; 429 | } 430 | .hpx-stepper-button-add { 431 | text-align: right; 432 | padding: .1rem .4rem; 433 | } 434 | .hpx-stepper-button-sub { 435 | padding: .1rem .4rem; 436 | } 437 | .hpx-stepper-number- { 438 | display: flex; 439 | font-weight: 700; 440 | align-items: center; 441 | justify-content: center; 442 | height: 100%; 443 | user-select: none; 444 | min-width: 32px; 445 | } 446 | .current { 447 | font-size: 115%; 448 | transform: translateY(0%); 449 | } 450 | """, '<', '>')} 451 | -------------------------------------------------------------------------------- /src/happyx_ui/progress.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core, 4 | ./colors 5 | 6 | 7 | proc Progress*(modifier: Modifier = initModifier(), class: string = "", 8 | state: State[int] = nil, maxVal: int = 100, stmt: TagRef = nil): TagRef = 9 | let w = 10 | if not state.isNil: 11 | state.val / maxVal 12 | else: 13 | 0.0 14 | buildHtml: 15 | tDiv(class = fmt"hpx-progress-back {class}"): 16 | tDiv(class = fmt"hpx-progress {class}") 17 | tStyle: {fmt(""" 18 | div.hpx-progress { 19 | position: absolute; 20 | width: %; 21 | height: 100%; 22 | background-color: ; 23 | } 24 | div.hpx-progress-back { 25 | position: relative; 26 | background-color: ; 27 | width: 256px; 28 | height: 6px; 29 | overflow: hidden; 30 | border-radius: 1rem; 31 | } 32 | """, '<', '>')} 33 | -------------------------------------------------------------------------------- /src/happyx_ui/surface.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core 4 | 5 | 6 | proc Surface*(modifier: Modifier = initModifier(), class: string = "", stmt: TagRef = nil): TagRef = 7 | buildHtml: 8 | tDiv(style = modifier.build(), class = class): 9 | if not stmt.isNil: 10 | stmt 11 | -------------------------------------------------------------------------------- /src/happyx_ui/tag.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core, 4 | ./colors 5 | 6 | 7 | let style = buildHtml: 8 | tStyle: {fmt(""" 9 | .hpx-tag { 10 | display: flex; 11 | border-radius: 1rem; 12 | border: 2px solid; 13 | background-color: 70; 14 | color: ; 15 | transition: all; 16 | transition-duration: .3s; 17 | white-space: nowrap; 18 | padding: .15rem .6rem; 19 | cursor: default; 20 | } 21 | .hpx-tag:hover { 22 | border-color: ; 23 | background-color: 70; 24 | } 25 | .hpx-tag:active { 26 | border-color: ; 27 | background-color: 70; 28 | } 29 | """, '<', '>')} 30 | document.head.appendChild(style.children[0]) 31 | 32 | 33 | proc Tag*(text: string = "", modifier: Modifier = initModifier(), 34 | class: string = "", stmt: TagRef = nil): TagRef = 35 | buildHtml: 36 | tDiv(class = "hpx-tag {class}"): 37 | if text.len > 0: 38 | {text} 39 | elif not stmt.isNil: 40 | stmt 41 | -------------------------------------------------------------------------------- /src/happyx_ui/text.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core 4 | 5 | 6 | let style = buildHtml: 7 | tStyle: """ 8 | p.hpx-title { 9 | font-size: 34px; 10 | font-weight: 700; 11 | margin: .4rem 0; 12 | } 13 | p.hpx-text { 14 | font-size: 18px; 15 | font-weight: 500; 16 | margin: 0; 17 | } 18 | """ 19 | document.head.appendChild(style.children[0]) 20 | 21 | 22 | proc Title*(text: string = "", modifier: Modifier = initModifier(), class: string = ""): TagRef = 23 | buildHtml: 24 | tP(class = "hpx-title " & class, style = modifier.build()): 25 | {text} 26 | 27 | 28 | proc Text*(text: string = "", modifier: Modifier = initModifier(), class: string = ""): TagRef = 29 | buildHtml: 30 | tP(class = "hpx-text " & class, style = modifier.build()): 31 | {text} 32 | -------------------------------------------------------------------------------- /src/happyx_ui/tooltip.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | ./core 4 | 5 | proc Tooltip*(modifier: Modifier = initModifier(), 6 | class: string = "", delay: float = 0.5, 7 | stmt: TagRef = nil): TagRef = 8 | let id = uid() 9 | buildHtml: 10 | tDiv(class = fmt"hpx-tooltip-{id} hpx-tooltip-off-{id} {class}", style = modifier.build()): 11 | if not stmt.isNil: 12 | stmt 13 | tScript: {fmt(""" 14 | document.querySelector(".hpx-tooltip-").parentElement._eventListeners = []; 15 | document.querySelector(".hpx-tooltip-").parentElement.addEventListener('mouseover', (ev) =>> { 16 | const e = document.querySelector(".hpx-tooltip-"); 17 | e.classList.toggle("hpx-tooltip-on-"); 18 | e.classList.toggle("hpx-tooltip-off-"); 19 | }); 20 | document.querySelector(".hpx-tooltip-").parentElement.addEventListener('mouseout', (ev) =>> { 21 | const e = document.querySelector(".hpx-tooltip-"); 22 | e.classList.toggle("hpx-tooltip-on-"); 23 | e.classList.toggle("hpx-tooltip-off-"); 24 | }); 25 | document.querySelector(".hpx-tooltip-").parentElement.addEventListener('mousemove', (ev) =>> { 26 | const e = document.querySelector(".hpx-tooltip-"); 27 | const bound = e.getBoundingClientRect(); 28 | e.style.left = `${ev.pageX}px`; 29 | e.style.top = `${ev.pageY}px`; 30 | if (bound.x + bound.width + 24 >> window.innerWidth) { 31 | e.classList.add("hpx-tooltip-x-"); 32 | } else if (bound.x + bound.width + bound.width + 48 << window.innerWidth) { 33 | e.classList.remove("hpx-tooltip-x-"); 34 | } 35 | if (!e.classList.contains("hpx-tooltip-y-")) { 36 | if (bound.y + bound.height + 24 >> window.innerHeight) { 37 | e.classList.add("hpx-tooltip-y-"); 38 | } 39 | } else if (bound.y + bound.height + bound.height + 48 << window.innerHeight) { 40 | e.classList.remove("hpx-tooltip-y-"); 41 | } 42 | if (e.classList.contains("hpx-tooltip-y-") && e.classList.contains("hpx-tooltip-x-")) { 43 | e.classList.remove("hpx-tooltip-y-"); 44 | e.classList.remove("hpx-tooltip-x-"); 45 | e.classList.add("hpx-tooltip-xy-"); 46 | } else if (e.classList.contains("hpx-tooltip-xy-")) { 47 | if (bound.y + bound.height + bound.height + 48 << window.innerHeight) { 48 | if (bound.y + bound.height + bound.height + 48 << window.innerHeight) { 49 | e.classList.remove("hpx-tooltip-xy-"); 50 | } 51 | } 52 | } 53 | }); 54 | """, '<', '>')} 55 | tStyle: {fmt(""" 56 | :has(>> div.hpx-tooltip-) { 57 | } 58 | div.hpx-tooltip- { 59 | position: absolute; 60 | z-index: 9; 61 | user-select: none; 62 | pointer-events: none; 63 | transition: opacity; 64 | transition-duration: .3s; 65 | transition-delay: s; 66 | margin-left: 24px; 67 | margin-top: 24px; 68 | } 69 | div.hpx-tooltip-on- { 70 | opacity: 1; 71 | } 72 | div.hpx-tooltip-off- { 73 | opacity: 0; 74 | transition-delay: .1s !important; 75 | } 76 | div.hpx-tooltip-x- { 77 | transform: translateX(-100%); 78 | margin-left: 0; 79 | margin-right: 24px; 80 | } 81 | div.hpx-tooltip-y- { 82 | transform: translateY(-100%); 83 | margin-bottom: 24px; 84 | margin-top: 0; 85 | } 86 | div.hpx-tooltip-xy- { 87 | transform: translateY(-100%) translateX(-100%); 88 | margin-left: 0; 89 | margin-right: 24px; 90 | margin-bottom: 24px; 91 | margin-top: 0; 92 | } 93 | """, '<', '>')} 94 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HappyX UI library 5 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import 2 | happyx, 3 | happyx_ui 4 | 5 | 6 | var 7 | name = remember "" 8 | age = remember 0 9 | isMale = remember true 10 | amount = remember 35 11 | stepperCount = remember 5 12 | 13 | 14 | appRoutes "app": 15 | "/": 16 | Surface(initModifier() 17 | .minWidth(100.vw) 18 | .minHeight(100.vh) 19 | .height("100%") 20 | .backgroundColor(BACKGROUND_COLOR) 21 | .textColor(FOREGROUND_COLOR) 22 | ): 23 | Column(): 24 | Title("Buttons 🧈") 25 | Row(1.rem): 26 | tDiv: 27 | Tooltip(delay = 1): 28 | Surface(initModifier() 29 | .borderRadius(0.5.rem) 30 | .padding(0.2.rem) 31 | .dropShadow(DropShadow.MD) 32 | .backgroundColor(BACKGROUND_COLOR) 33 | ): 34 | Text("Hello world") 35 | Button(proc() = 36 | echo "default button was clicked" 37 | ): 38 | "default button" 39 | OutlineButton(proc() = 40 | echo "outline button was clicked" 41 | ): 42 | "outline button" 43 | Title("Cards 🎴") 44 | Row(1.rem): 45 | Card(): 46 | Column(1.rem): 47 | Title("Hello, world!") 48 | Text("There are input elements") 49 | Row(1.rem): 50 | Input(state = name, id = "username", hint = "First name"): 51 | # placeholder 52 | "Who are you?" 53 | OutlineInput(id = "username2", hint = "Last name"): 54 | # placeholder 55 | "What's your last name?" 56 | Text("There are checkboxes") 57 | Checkbox(state = isMale): 58 | "Are you male?" 59 | Text("Progress bar 👀") 60 | Progress(state = amount) 61 | Card(): 62 | Column(1.rem): 63 | Title("Other Inputs") 64 | Text("switchers") 65 | Switcher(state = isMale): 66 | "Are you male?" 67 | Text("Stepper") 68 | Stepper(state = stepperCount, min = 0, max = 10, step = 1) 69 | Card(modifier = initModifier() 70 | .padding(0.px) 71 | .overflowHidden() 72 | ): 73 | Image( 74 | "https://images.kinorium.com/movie/poster/2409863/w1500_52078438.jpg", 75 | modifier = initModifier() 76 | .width(200.px) 77 | .height(240.px) 78 | .objectCover() 79 | .objectCenter() 80 | ) 81 | Column(modifier = initModifier().padding(4.px, 0.px, 4.px, 10.px)): 82 | Title("Fallout") 83 | Row(modifier = initModifier().width(190.px).flexWrap()): 84 | Tag("science fiction") 85 | Tag("action") 86 | Tag("drama") 87 | Tag("adventure") 88 | Title("Containers 👀") 89 | Row(2.rem): 90 | Column(): 91 | Text("Box container") 92 | Box( 93 | horizontal = "center", vertical = "center", 94 | modifier = initModifier() 95 | .width(256.px) 96 | .height(256.px) 97 | ): 98 | ChildModifier(initModifier().borderRadius(1.rem)): 99 | Surface(initModifier().width(200.px).height(200.px).backgroundColor(PRIMARY_COLOR)) 100 | Surface(initModifier().width(150.px).height(150.px).backgroundColor(PRIMARY_HOVER_COLOR)) 101 | Surface(initModifier().width(100.px).height(100.px).backgroundColor(PRIMARY_ACTIVE_COLOR)) 102 | Column(): 103 | Text("Row container") 104 | Row( 105 | horizontal = "center", vertical = "center", 106 | modifier = initModifier() 107 | .width(256.px) 108 | .height(256.px) 109 | ): 110 | ChildModifier(initModifier().borderRadius(1.rem)): 111 | Surface(initModifier().width(96.px).height(96.px).backgroundColor(PRIMARY_COLOR)) 112 | Surface(initModifier().width(64.px).height(64.px).backgroundColor(PRIMARY_HOVER_COLOR)) 113 | Surface(initModifier().width(32.px).height(32.px).backgroundColor(PRIMARY_ACTIVE_COLOR)) 114 | Column(): 115 | Text("Column container") 116 | Column( 117 | horizontal = "center", vertical = "center", 118 | modifier = initModifier() 119 | .width(256.px) 120 | .height(256.px) 121 | ): 122 | ChildModifier(initModifier().borderRadius(1.rem)): 123 | Surface(initModifier().width(96.px).height(96.px).backgroundColor(PRIMARY_COLOR)) 124 | Surface(initModifier().width(64.px).height(64.px).backgroundColor(PRIMARY_HOVER_COLOR)) 125 | Surface(initModifier().width(32.px).height(32.px).backgroundColor(PRIMARY_ACTIVE_COLOR)) 126 | Column(): 127 | Text("Column container") 128 | Grid(cols = 3): 129 | ChildModifier(initModifier().borderRadius(1.rem)): 130 | Surface(initModifier().width("100%").height("100%").colSpan(2).backgroundColor(PRIMARY_COLOR)) 131 | Surface(initModifier().width(64.px).height(64.px).backgroundColor(PRIMARY_HOVER_COLOR)) 132 | Surface(initModifier().width(64.px).height(64.px).backgroundColor(PRIMARY_ACTIVE_COLOR)) 133 | Surface(initModifier().width("100%").height("100%").colSpan(2).backgroundColor(PRIMARY_COLOR)) 134 | Tooltip: 135 | "Hello" 136 | 137 | "/testing": 138 | tDiv: 139 | Column(1.em): 140 | Row(2.rem): 141 | Button( 142 | proc() = 143 | echo "Hello, world!" 144 | ): 145 | "Hello, world!" 146 | OutlineButton( 147 | proc() = 148 | echo "Hello, world!" 149 | ): 150 | "Hello, world!" 151 | 152 | # Will be insert as 153 | Image( 154 | "https://www.svgrepo.com/show/530485/the-internet.svg", 155 | width = 256.px, height = 125.px 156 | ) 157 | # Will be insert as 158 | Image( 159 | "https://i.pinimg.com/originals/9c/f0/62/9cf062842a21964001796f28d3ba8c22.png", 160 | width = 256.px, height = 125.px 161 | ) 162 | 163 | # Test modifiers 164 | Surface( 165 | initModifier() 166 | .padding(1.rem, 2.rem) 167 | .background(FOREGROUND_COLOR) 168 | .width(128.px) 169 | .height(128.px) 170 | .borderRadius(1.rem) 171 | .borderStyle("solid") 172 | .borderColor(BACKGROUND_COLOR) 173 | .borderWidth(8.px) 174 | .textColor(BACKGROUND_COLOR) 175 | ): 176 | Column( 177 | horizontal = "center", 178 | vertical = "center", 179 | modifier = initModifier() 180 | .height("100%") 181 | ): 182 | "Hello, world!" 183 | -------------------------------------------------------------------------------- /tests/test.nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src" 2 | --------------------------------------------------------------------------------