├── .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 | [](https://hapticx.github.io/happyx-ui/#/)
7 |
8 |
9 | [](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 | 
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