├── .dockerignore
├── .gitattributes
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .vscode
├── settings.json
└── tasks.json
├── Dockerfile
├── README.md
├── fly.toml
├── gleam.toml
├── manifest.toml
├── priv
└── static
│ ├── assets
│ ├── favicon.ico
│ ├── gleam-logo.jpg
│ └── main.css
│ └── vendor
│ └── htmx.min.js
├── src
├── todomvc.gleam
├── todomvc
│ ├── database.gleam
│ ├── error.gleam
│ ├── item.gleam
│ ├── router.gleam
│ ├── templates
│ │ ├── completed_cleared.gleam
│ │ ├── completed_cleared.matcha
│ │ ├── home.gleam
│ │ ├── home.matcha
│ │ ├── item.gleam
│ │ ├── item.matcha
│ │ ├── item_changed.gleam
│ │ ├── item_changed.matcha
│ │ ├── item_created.gleam
│ │ ├── item_created.matcha
│ │ ├── item_deleted.gleam
│ │ └── item_deleted.matcha
│ ├── user.gleam
│ └── web.gleam
└── todomvc_ffi.erl
└── test
├── todomvc
├── item_test.gleam
├── routes_test.gleam
├── tests.gleam
└── user_test.gleam
├── todomvc_test.gleam
└── todomvc_test_helper.erl
/.dockerignore:
--------------------------------------------------------------------------------
1 | build
2 | bin
3 | test
4 | README.md
5 | secrets.env
6 | Dockerfile
7 | fly.toml
8 | .github
9 | .gitignore
10 | .git
11 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.matcha linguist-language=html
2 | src/todomvc/templates/**/*.gleam linguist-generated=true
3 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - main
8 | pull_request:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: erlef/setup-beam@v1
16 | with:
17 | otp-version: "26.0"
18 | gleam-version: "1.3.2"
19 | rebar3-version: "3"
20 | - run: gleam format --check src test
21 | - run: gleam test
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.beam
2 | *.ez
3 | *.sqlite3
4 | build
5 | erl_crash.dump
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {"*.matcha": "html"}
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "gleam test",
8 | "type": "shell",
9 | "command": "gleam test",
10 | "problemMatcher": [],
11 | "group": {
12 | "kind": "test",
13 | "isDefault": true
14 | },
15 | "presentation": {
16 | "echo": false,
17 | "reveal": "always",
18 | "focus": false,
19 | "panel": "shared",
20 | "showReuseMessage": false,
21 | "clear": true
22 | }
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/gleam-lang/gleam:v1.3.2-erlang-alpine
2 |
3 | # Add project code
4 | COPY . /build/
5 |
6 | # Compile the Gleam application
7 | RUN apk add gcc build-base \
8 | && cd /build \
9 | && gleam export erlang-shipment \
10 | && mv build/erlang-shipment /app \
11 | && rm -r /build \
12 | && apk del gcc build-base \
13 | && addgroup -S todomvc \
14 | && adduser -S todomvc -G todomvc \
15 | && chown -R todomvc /app
16 |
17 | # Run the application
18 | USER todomvc
19 | WORKDIR /app
20 | ENTRYPOINT ["/app/entrypoint.sh"]
21 | CMD ["run"]
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TodoMVC
2 |
3 | An example codebase that shows how to create a web application in Gleam. It is a
4 | backend based implementation of [TodoMVC](https://todomvc.com/) and demonstrates
5 | these features:
6 |
7 | - A HTTP server
8 | - Routing
9 | - CRUD
10 | - Use of a SQLite database
11 | - HTML templates
12 | - Form parsing
13 | - Signed cookie based authentication
14 | - Serving static assets
15 | - Logging
16 | - Testing
17 |
18 | Rather than demonstrate any particular frontend web framework this project uses
19 | [HTMX](https://htmx.org/), a library that adds some new HTML attributes for
20 | declaratively performing AJAX requests.
21 |
22 | ## HTML templates
23 |
24 | The HTML templates are compiled using [matcha](https://github.com/michaeljones/matcha).
25 |
26 | To regenerate the Gleam code from the templates run:
27 |
28 | ```shell
29 | matcha && gleam format .
30 | ```
31 |
32 | ## Thanks
33 |
34 | Special thanks to [GregGreg](https://gitlab.com/greggreg/gleam_todo) for the
35 | first version of TodoMVC in Gleam.
36 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | app = "gleam-todomvc"
2 | kill_signal = "SIGINT"
3 | kill_timeout = 5
4 | processes = []
5 |
6 | [env]
7 |
8 | [experimental]
9 | allowed_public_ports = []
10 | auto_rollback = true
11 | private_network = true
12 |
13 | [deploy]
14 | strategy = "canary"
15 |
16 | [[services]]
17 | http_checks = []
18 | internal_port = 3000
19 | processes = ["app"]
20 | protocol = "tcp"
21 | script_checks = []
22 |
23 | [services.concurrency]
24 | hard_limit = 25
25 | soft_limit = 20
26 | type = "connections"
27 |
28 | [[services.ports]]
29 | handlers = ["http"]
30 | port = 80
31 |
32 | [[services.ports]]
33 | handlers = ["tls", "http"]
34 | port = 443
35 |
36 | [[services.tcp_checks]]
37 | grace_period = "1s"
38 | interval = "15s"
39 | restart_limit = 0
40 | timeout = "2s"
41 |
--------------------------------------------------------------------------------
/gleam.toml:
--------------------------------------------------------------------------------
1 | name = "todomvc"
2 | version = "0.1.0"
3 | gleam = ">= 0.32.0"
4 |
5 | # Fill out these fields if you intend to generate HTML documentation or publish
6 | # your project to the Hex package manager.
7 | #
8 | # licences = ["Apache-2.0"]
9 | # description = "A Gleam library..."
10 | # repository = { type = "github", user = "username", repo = "project" }
11 | # links = [{ title = "Website", href = "https://gleam.run" }]
12 |
13 | [dependencies]
14 | gleam_stdlib = "~> 0.31"
15 | gleam_http = "~> 3.5"
16 | gleam_erlang = "~> 0.22"
17 | gleam_crypto = "~> 1.0"
18 | mist = "~> 1.2"
19 | sqlight = "~> 0.8"
20 | wisp = "~> 0.5"
21 |
22 | [dev-dependencies]
23 | gleeunit = "~> 1.2"
24 |
--------------------------------------------------------------------------------
/manifest.toml:
--------------------------------------------------------------------------------
1 | # This file was generated by Gleam
2 | # You typically do not need to edit this file
3 |
4 | packages = [
5 | { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" },
6 | { name = "esqlite", version = "0.8.8", build_tools = ["rebar3"], requirements = [], otp_app = "esqlite", source = "hex", outer_checksum = "374902457C7D94DC9409C98D3BDD1CA0D50A60DC9F3BDF1FD8EB74C0DCDF02D6" },
7 | { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" },
8 | { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
9 | { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" },
10 | { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
11 | { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" },
12 | { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
13 | { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
14 | { name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
15 | { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" },
16 | { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" },
17 | { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" },
18 | { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" },
19 | { name = "logging", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "FCB111401BDB4703A440A94FF8CC7DA521112269C065F219C2766998333E7738" },
20 | { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" },
21 | { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" },
22 | { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
23 | { name = "simplifile", version = "2.0.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "5FFEBD0CAB39BDD343C3E1CCA6438B2848847DC170BA2386DF9D7064F34DF000" },
24 | { name = "sqlight", version = "0.9.0", build_tools = ["gleam"], requirements = ["esqlite", "gleam_stdlib"], otp_app = "sqlight", source = "hex", outer_checksum = "2D9C9BA420A5E7DCE7DB2DAAE4CAB0BE6218BEB48FD1531C583550B3D1316E94" },
25 | { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" },
26 | { name = "wisp", version = "0.15.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "33D17A50276FE0A10E4F694E4EF7D99836954DC2D920D4B5741B1E0EBCAE403F" },
27 | ]
28 |
29 | [requirements]
30 | gleam_crypto = { version = "~> 1.0" }
31 | gleam_erlang = { version = "~> 0.22" }
32 | gleam_http = { version = "~> 3.5" }
33 | gleam_stdlib = { version = "~> 0.31" }
34 | gleeunit = { version = "~> 1.2" }
35 | mist = { version = "~> 1.2" }
36 | sqlight = { version = "~> 0.8" }
37 | wisp = { version = "~> 0.5" }
38 |
--------------------------------------------------------------------------------
/priv/static/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gleam-lang/example-todomvc/6087b311ae45b57a3a63378a7a8062efe7d82f34/priv/static/assets/favicon.ico
--------------------------------------------------------------------------------
/priv/static/assets/gleam-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gleam-lang/example-todomvc/6087b311ae45b57a3a63378a7a8062efe7d82f34/priv/static/assets/gleam-logo.jpg
--------------------------------------------------------------------------------
/priv/static/assets/main.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 |
143 |
144 | html,
145 | body {
146 | margin: 0;
147 | padding: 0;
148 | }
149 |
150 | button {
151 | margin: 0;
152 | padding: 0;
153 | border: 0;
154 | background: none;
155 | font-size: 100%;
156 | vertical-align: baseline;
157 | font-family: inherit;
158 | font-weight: inherit;
159 | color: inherit;
160 | -webkit-appearance: none;
161 | appearance: none;
162 | -webkit-font-smoothing: antialiased;
163 | -moz-osx-font-smoothing: grayscale;
164 | }
165 |
166 | body {
167 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
168 | line-height: 1.4em;
169 | background: #f5f5f5;
170 | color: #4d4d4d;
171 | min-width: 230px;
172 | max-width: 550px;
173 | margin: 0 auto;
174 | -webkit-font-smoothing: antialiased;
175 | -moz-osx-font-smoothing: grayscale;
176 | font-weight: 300;
177 | }
178 |
179 | :focus {
180 | outline: 0;
181 | }
182 |
183 | .hidden {
184 | display: none;
185 | }
186 |
187 | .todoapp {
188 | background: #fff;
189 | margin: 130px 0 40px 0;
190 | position: relative;
191 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
192 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
193 | }
194 |
195 | .todoapp input::-webkit-input-placeholder {
196 | font-style: italic;
197 | font-weight: 300;
198 | color: #e6e6e6;
199 | }
200 |
201 | .todoapp input::-moz-placeholder {
202 | font-style: italic;
203 | font-weight: 300;
204 | color: #e6e6e6;
205 | }
206 |
207 | .todoapp input::input-placeholder {
208 | font-style: italic;
209 | font-weight: 300;
210 | color: #e6e6e6;
211 | }
212 |
213 | .todoapp h1 {
214 | position: absolute;
215 | top: -155px;
216 | width: 100%;
217 | font-size: 100px;
218 | font-weight: 100;
219 | text-align: center;
220 | color: rgba(175, 47, 47, 0.15);
221 | -webkit-text-rendering: optimizeLegibility;
222 | -moz-text-rendering: optimizeLegibility;
223 | text-rendering: optimizeLegibility;
224 | }
225 |
226 | .new-todo,
227 | .edit {
228 | position: relative;
229 | margin: 0;
230 | width: 100%;
231 | font-size: 24px;
232 | font-family: inherit;
233 | font-weight: inherit;
234 | line-height: 1.4em;
235 | border: 0;
236 | color: inherit;
237 | padding: 6px;
238 | border: 1px solid #999;
239 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
240 | box-sizing: border-box;
241 | -webkit-font-smoothing: antialiased;
242 | -moz-osx-font-smoothing: grayscale;
243 | }
244 |
245 | .new-todo {
246 | padding: 16px 16px 16px 60px;
247 | border: none;
248 | background: rgba(0, 0, 0, 0.003);
249 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
250 | }
251 |
252 | .main {
253 | position: relative;
254 | z-index: 2;
255 | border-top: 1px solid #e6e6e6;
256 | }
257 |
258 | .toggle-all {
259 | text-align: center;
260 | border: none; /* Mobile Safari */
261 | opacity: 0;
262 | position: absolute;
263 | }
264 |
265 | .toggle-all + label {
266 | width: 60px;
267 | height: 34px;
268 | font-size: 0;
269 | position: absolute;
270 | top: -52px;
271 | left: -13px;
272 | -webkit-transform: rotate(90deg);
273 | transform: rotate(90deg);
274 | }
275 |
276 | .toggle-all + label:before {
277 | content: '❯';
278 | font-size: 22px;
279 | color: #e6e6e6;
280 | padding: 10px 27px 10px 27px;
281 | }
282 |
283 | .toggle-all:checked + label:before {
284 | color: #737373;
285 | }
286 |
287 | .todo-list {
288 | margin: 0;
289 | padding: 0;
290 | list-style: none;
291 | }
292 |
293 | .todo-list li {
294 | position: relative;
295 | font-size: 24px;
296 | border-bottom: 1px solid #ededed;
297 | }
298 |
299 | .todo-list li:last-child {
300 | border-bottom: none;
301 | }
302 |
303 | .todo-list li.editing {
304 | border-bottom: none;
305 | padding: 0;
306 | }
307 |
308 | .todo-list li.editing .edit {
309 | display: block;
310 | width: 506px;
311 | padding: 12px 16px;
312 | margin: 0 0 0 43px;
313 | }
314 |
315 | .todo-list li.editing .view {
316 | display: none;
317 | }
318 |
319 | .todo-list li .toggle {
320 | text-align: center;
321 | width: 40px;
322 | /* auto, since non-WebKit browsers doesn't support input styling */
323 | height: auto;
324 | position: absolute;
325 | top: 0;
326 | bottom: 0;
327 | margin: auto 0;
328 | border: none; /* Mobile Safari */
329 | -webkit-appearance: none;
330 | appearance: none;
331 | }
332 |
333 | .todo-list li .toggle {
334 | opacity: 0;
335 | }
336 |
337 | .todo-list li .toggle + label {
338 | /*
339 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
340 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
341 | */
342 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
343 | background-repeat: no-repeat;
344 | background-position: center left;
345 | }
346 |
347 | .todo-list li .toggle:checked + label {
348 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
349 | }
350 |
351 | .todo-list li label {
352 | word-break: break-all;
353 | padding: 15px 15px 15px 60px;
354 | display: block;
355 | line-height: 1.2;
356 | transition: color 0.4s;
357 | }
358 |
359 | .todo-list li.completed label {
360 | color: #d9d9d9;
361 | text-decoration: line-through;
362 | }
363 |
364 | .todo-list li .destroy {
365 | display: none;
366 | position: absolute;
367 | top: 0;
368 | right: 10px;
369 | bottom: 0;
370 | width: 40px;
371 | height: 40px;
372 | margin: auto 0;
373 | font-size: 30px;
374 | color: #cc9a9a;
375 | margin-bottom: 11px;
376 | transition: color 0.2s ease-out;
377 | }
378 |
379 | .todo-list li .destroy:hover {
380 | display: block;
381 | color: #af5b5e;
382 | cursor: pointer;
383 | }
384 |
385 | .todo-list li .destroy:after {
386 | content: 'x';
387 | }
388 |
389 | .todo-list li:hover .destroy {
390 | display: block;
391 | }
392 |
393 | .todo-list li .edit {
394 | display: none;
395 | }
396 |
397 | .todo-list li.editing:last-child {
398 | margin-bottom: -1px;
399 | }
400 |
401 | .footer {
402 | color: #777;
403 | padding: 10px 15px;
404 | height: 20px;
405 | text-align: center;
406 | border-top: 1px solid #e6e6e6;
407 | }
408 |
409 | .footer:before {
410 | content: '';
411 | position: absolute;
412 | right: 0;
413 | bottom: 0;
414 | left: 0;
415 | height: 50px;
416 | overflow: hidden;
417 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
418 | 0 8px 0 -3px #f6f6f6,
419 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
420 | 0 16px 0 -6px #f6f6f6,
421 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
422 | }
423 |
424 | .todo-count {
425 | float: left;
426 | text-align: left;
427 | }
428 |
429 | .todo-count strong {
430 | font-weight: 300;
431 | }
432 |
433 | .filters {
434 | margin: 0;
435 | padding: 0;
436 | list-style: none;
437 | position: absolute;
438 | right: 0;
439 | left: 0;
440 | }
441 |
442 | .filters li {
443 | display: inline;
444 | }
445 |
446 | .filters li a {
447 | color: inherit;
448 | margin: 3px;
449 | padding: 3px 7px;
450 | text-decoration: none;
451 | border: 1px solid transparent;
452 | border-radius: 3px;
453 | }
454 |
455 | .filters li a:hover {
456 | border-color: rgba(175, 47, 47, 0.1);
457 | }
458 |
459 | .filters li a.selected {
460 | border-color: rgba(175, 47, 47, 0.2);
461 | }
462 |
463 | .clear-completed,
464 | html .clear-completed:active {
465 | float: right;
466 | position: relative;
467 | line-height: 20px;
468 | text-decoration: none;
469 | cursor: pointer;
470 | }
471 |
472 | .clear-completed:hover {
473 | text-decoration: underline;
474 | }
475 |
476 | .info {
477 | margin: 65px auto 0;
478 | color: #bfbfbf;
479 | font-size: 10px;
480 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
481 | text-align: center;
482 | }
483 |
484 | .info p {
485 | line-height: 1;
486 | }
487 |
488 | .info a {
489 | color: inherit;
490 | text-decoration: none;
491 | font-weight: 400;
492 | }
493 |
494 | .info a:hover {
495 | text-decoration: underline;
496 | }
497 |
498 | /*
499 | Hack to remove background from Mobile Safari.
500 | Can't use it globally since it destroys checkboxes in Firefox
501 | */
502 | @media screen and (-webkit-min-device-pixel-ratio:0) {
503 | .toggle-all,
504 | .todo-list li .toggle {
505 | background: none;
506 | }
507 |
508 | .todo-list li .toggle {
509 | height: 40px;
510 | }
511 | }
512 |
513 | @media (max-width: 430px) {
514 | .footer {
515 | height: 50px;
516 | }
517 |
518 | .filters {
519 | bottom: 10px;
520 | }
521 | }
522 |
523 |
524 | /* override styles */
525 | form.todo-mark {
526 | position: absolute;
527 | top: 0;
528 | bottom: 0;
529 | left: 0;
530 | width: 49px;
531 | }
532 |
533 | form.todo-mark button {
534 | height: 100%;
535 | width: 100%;
536 | cursor: pointer;
537 | }
538 |
539 | #logo {
540 | width: 26%
541 | }
542 |
543 | .todo-list li .edit-btn {
544 | display: none;
545 | position: absolute;
546 | top: 0;
547 | right: 36px;
548 | bottom: 0;
549 | width: 40px;
550 | height: 40px;
551 | margin: auto 0;
552 | margin-bottom: auto;
553 | font-size: 26px;
554 | color: #cc9a9a;
555 | margin-bottom: -3px;
556 | transition: color 0.2s ease-out;
557 | text-decoration: none;
558 | }
559 |
560 | .todo-list li:hover .edit-btn {
561 | display: block;
562 | cursor: pointer;
563 | }
564 |
565 | .todo-list li .edit-btn:hover {
566 | color: #af5b5e;
567 | }
568 |
--------------------------------------------------------------------------------
/priv/static/vendor/htmx.min.js:
--------------------------------------------------------------------------------
1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else{e.htmx=t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var D={onLoad:t,process:rt,on:N,off:I,trigger:lt,ajax:$t,find:w,findAll:S,closest:O,values:function(e,t){var r=Ot(e,t||"post");return r.values},remove:E,addClass:C,removeClass:R,toggleClass:q,takeClass:L,defineExtension:Qt,removeExtension:er,logAll:b,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth"},parseInterval:h,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){return new WebSocket(e,[])},version:"1.6.1"};var r=["get","post","put","delete","patch"];var n=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function h(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}return parseFloat(e)||undefined}function c(e,t){return e.getAttribute&&e.getAttribute(t)}function s(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function F(e,t){return c(e,t)||c(e,"data-"+t)}function l(e){return e.parentElement}function P(){return document}function d(e,t){if(t(e)){return e}else if(l(e)){return d(l(e),t)}else{return null}}function X(e,t){var r=null;d(e,function(e){return r=F(e,t)});if(r!=="unset"){return r}}function v(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function i(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function o(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=P().createDocumentFragment()}return i}function u(e){if(D.config.useTemplateFragments){var t=o("
"+e+"",0);return t.querySelector("template").content}else{var r=i(e);switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return o("",1);case"col":return o("",2);case"tr":return o("",2);case"td":case"th":return o("",3);case"script":return o(""+e+"
",1);default:return o(e,0)}}}function U(e){if(e){e()}}function a(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function f(e){return a(e,"Function")}function g(e){return a(e,"Object")}function j(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function p(e){var t=[];if(e){for(var r=0;r=0}function z(e){return P().body.contains(e)}function y(e){return e.trim().split(/\s+/)}function V(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function x(e){try{return JSON.parse(e)}catch(e){ut(e);return null}}function e(e){return Ut(P().body,function(){return eval(e)})}function t(t){var e=D.on("htmx:load",function(e){t(e.detail.elt)});return e}function b(){D.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function w(e,t){if(t){return e.querySelector(t)}else{return w(P(),e)}}function S(e,t){if(t){return e.querySelectorAll(t)}else{return S(P(),e)}}function E(e,t){e=H(e);if(t){setTimeout(function(){E(e)},t)}else{e.parentElement.removeChild(e)}}function C(e,t,r){e=H(e);if(r){setTimeout(function(){C(e,t)},r)}else{e.classList&&e.classList.add(t)}}function R(e,t,r){e=H(e);if(r){setTimeout(function(){R(e,t)},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function q(e,t){e=H(e);e.classList.toggle(t)}function L(e,t){e=H(e);B(e.parentElement.children,function(e){R(e,t)});C(e,t)}function O(e,t){e=H(e);if(e.closest){return e.closest(t)}else{do{if(e==null||v(e,t)){return e}}while(e=e&&l(e))}}function T(e,t){if(t.indexOf("closest ")===0){return[O(e,t.substr(8))]}else if(t.indexOf("find ")===0){return[w(e,t.substr(5))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return P().querySelectorAll(t)}}function A(e,t){if(t){return T(e,t)[0]}else{return T(P().body,e)[0]}}function H(e){if(a(e,"String")){return w(e)}else{return e}}function k(e,t,r){if(f(t)){return{target:P().body,event:e,listener:t}}else{return{target:H(e),event:t,listener:r}}}function N(t,r,n){rr(function(){var e=k(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=f(r);return e?r:n}function I(t,r,n){rr(function(){var e=k(t,r,n);e.target.removeEventListener(e.event,e.listener)});return f(r)?r:n}function _(e){var t=d(e,function(e){return F(e,"hx-target")!==null});if(t){var r=F(t,"hx-target");if(r==="this"){return t}else{return A(e,r)}}else{var n=j(e);if(n.boosted){return P().body}else{return e}}}function M(e){var t=D.config.attributesToSettle;for(var r=0;r0){i=e.substr(0,e.indexOf(":"));n=e.substr(e.indexOf(":")+1,e.length)}else{i=e}var o=P().querySelector(n);if(o){var a;a=P().createDocumentFragment();a.appendChild(t);if(!$(i,o)){a=t}le(i,o,o,a,r)}else{t.parentNode.removeChild(t);ot(P().body,"htmx:oobErrorNoTarget",{content:t})}return e}function Z(e,r){B(S(e,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=F(e,"hx-swap-oob");if(t!=null){J(t,e,r)}})}function G(e){B(S(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=F(e,"id");var r=P().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function K(n,e,i){B(e.querySelectorAll("[id]"),function(e){if(e.id&&e.id.length>0){var t=n.querySelector(e.tagName+"[id='"+e.id+"']");if(t&&t!==n){var r=e.cloneNode();W(e,t);i.tasks.push(function(){W(e,r)})}}})}function Y(e){return function(){R(e,D.config.addedClass);rt(e);Ke(e);Q(e);lt(e,"htmx:load")}}function Q(e){var t="[autofocus]";var r=v(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function ee(e,t,r,n){K(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;C(i,D.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Y(i))}}}function te(t){var e=j(t);if(e.webSocket){e.webSocket.close()}if(e.sseEventSource){e.sseEventSource.close()}if(e.listenerInfos){B(e.listenerInfos,function(e){if(t!==e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(t.children){B(t.children,function(e){te(e)})}}function re(e,t,r){if(e.tagName==="BODY"){return se(e,t,r)}else{var n=e.previousSibling;ee(l(e),e,t,r);if(n==null){var i=l(e).firstChild}else{var i=n.nextSibling}j(e).replacedWith=i;r.elts=[];while(i&&i!==e){if(i.nodeType===Node.ELEMENT_NODE){r.elts.push(i)}i=i.nextElementSibling}te(e);l(e).removeChild(e)}}function ne(e,t,r){return ee(e,e.firstChild,t,r)}function ie(e,t,r){return ee(l(e),e,t,r)}function oe(e,t,r){return ee(e,null,t,r)}function ae(e,t,r){return ee(l(e),e.nextSibling,t,r)}function se(e,t,r){var n=e.firstChild;ee(e,n,t,r);if(n){while(n.nextSibling){te(n.nextSibling);e.removeChild(n.nextSibling)}te(n);e.removeChild(n)}}function ue(e,t){var r=X(e,"hx-select");if(r){var n=P().createDocumentFragment();B(t.querySelectorAll(r),function(e){n.appendChild(e)});t=n}return t}function le(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":re(r,n,i);return;case"afterbegin":ne(r,n,i);return;case"beforebegin":ie(r,n,i);return;case"beforeend":oe(r,n,i);return;case"afterend":ae(r,n,i);return;default:var o=tr(t);for(var a=0;a-1){var t=e.replace(/