├── priv
├── static
│ ├── assets
│ │ ├── js
│ │ │ └── input_handler.js
│ │ └── app.css
│ └── favicon.ico
└── css
│ └── app.css
├── test
├── test_helper.exs
└── boilerex_test.exs
├── .formatter.exs
├── lib
├── mix
│ └── tasks
│ │ ├── migrate.ex
│ │ └── just.ex
├── app.ex
├── core
│ ├── repo
│ │ ├── connection.ex
│ │ ├── helper.ex
│ │ └── task_repo.ex
│ └── services
│ │ └── task.ex
└── web
│ ├── views
│ ├── pages
│ │ └── index.ex
│ ├── layouts
│ │ └── main.ex
│ └── components
│ │ ├── icons.ex
│ │ └── task.ex
│ └── router
│ └── main.ex
├── config
└── config.exs
├── README.md
├── .gitignore
├── mix.exs
├── t.html
└── mix.lock
/priv/static/assets/js/input_handler.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/priv/css/app.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | :root {
4 | @apply bg-zinc-900 text-white;
5 | }
6 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ethanhamilthon/boilerex/main/priv/static/favicon.ico
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | # Used by "mix format"
2 | [
3 | import_deps: [:temple],
4 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
5 | ]
6 |
--------------------------------------------------------------------------------
/test/boilerex_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BoilerexTest do
2 | use ExUnit.Case
3 | doctest Boilerex
4 |
5 | test "greets the world" do
6 | assert Boilerex.hello() == :world
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/mix/tasks/migrate.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Migrate do
2 | alias Core.Repo.Connection
3 | alias Core.Repo.TaskRepo
4 | @shortdoc "Simple migration mix task"
5 |
6 | use Mix.Task
7 |
8 | @impl Mix.Task
9 | def run(_args) do
10 | Connection.start_link(0)
11 | TaskRepo.migrate()
12 | IO.puts("migrated 🚀")
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | import Config
2 |
3 | config :tailwind,
4 | version: "4.0.9",
5 | default: [
6 | args: ~w(
7 | --input=priv/css/app.css
8 | --output=priv/static/assets/app.css
9 | --watch
10 | ),
11 | cd: Path.expand("..", __DIR__)
12 | ]
13 |
14 | config :boilerex,
15 | port: 4089,
16 | db_path: "data.db"
17 |
18 | config :temple,
19 | engine: EEx.SmartEngine,
20 | attributes: {Temple, :attributes}
21 |
--------------------------------------------------------------------------------
/lib/mix/tasks/just.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.Tasks.Just do
2 | alias Web.Views.Components
3 | alias Core.Repo.Connection
4 | alias Core.Repo.TaskRepo
5 | @shortdoc "mix task for testing some things"
6 |
7 | use Mix.Task
8 |
9 | @impl Mix.Task
10 | def run(_args) do
11 | Connection.start_link(0)
12 |
13 | Components.Task.list(tasks: TaskRepo.list())
14 | |> Phoenix.HTML.Safe.to_iodata()
15 | |> IO.iodata_to_binary()
16 | |> IO.inspect()
17 |
18 | IO.puts("hello")
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/app.ex:
--------------------------------------------------------------------------------
1 | defmodule Boilerex.Application do
2 | alias Core.Repo.Connection
3 | use Application
4 |
5 | @impl true
6 | def start(_type, _args) do
7 | port = Application.get_env(:boilerex, :port, 4089)
8 | "server started in http://localhost:#{port}/" |> IO.puts()
9 |
10 | children = [
11 | {Plug.Cowboy, scheme: :http, plug: Web.Router.Main, options: [port: port]},
12 | Connection
13 | ]
14 |
15 | opts = [strategy: :one_for_one, name: Boilerex.Supervisor]
16 |
17 | Supervisor.start_link(children, opts)
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/core/repo/connection.ex:
--------------------------------------------------------------------------------
1 | defmodule Core.Repo.Connection do
2 | use GenServer
3 |
4 | def start_link(_) do
5 | db_path = Application.get_env(:boilerex, :db_path)
6 | GenServer.start_link(__MODULE__, db_path, name: __MODULE__)
7 | end
8 |
9 | def get_conn() do
10 | GenServer.call(__MODULE__, :get_conn)
11 | end
12 |
13 | @impl true
14 | def init(db_path) do
15 | {:ok, conn} = Exqlite.Sqlite3.open(db_path)
16 | {:ok, conn}
17 | end
18 |
19 | @impl true
20 | def handle_call(:get_conn, _from, conn) do
21 | {:reply, conn, conn}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Boilerex
2 |
3 | Simple starter template to create web apps with Elixir.
4 | Tech stack: Elixir, HTMX, Temple, Tailwind
5 |
6 | ## Running in DEV
7 |
8 | 1. Install deps and tailwind
9 | ```bash
10 | mix deps.get
11 | mix tailwind.install
12 | ```
13 |
14 | 2. Run db and tables
15 | ```bash
16 | mix migrate
17 | ```
18 |
19 | 3. Run tailwind and app itself in dev mode
20 | ```bash
21 | mix tailwind default
22 | iex -S mix
23 | ```
24 |
25 | ## Convert your html templates to elixir code
26 |
27 | 1. Put your html markups to t.html file first
28 |
29 | 2. Run the command
30 | ```bash
31 | mix temple.convert t.html
32 | ```
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # If the VM crashes, it generates a dump, let's ignore it too.
14 | erl_crash.dump
15 |
16 | # Also ignore archive artifacts (built via "mix archive.build").
17 | *.ez
18 |
19 | # Ignore package tarball (built via "mix hex.build").
20 | boilerex-*.tar
21 |
22 | # Temporary files, for example, from tests.
23 | /tmp/
24 |
25 | data.db
26 | .elixir_ls
27 | just.exs
28 |
--------------------------------------------------------------------------------
/lib/core/repo/helper.ex:
--------------------------------------------------------------------------------
1 | defmodule Core.Repo.Helper do
2 | alias Exqlite.Sqlite3
3 |
4 | def fetch_all_rows(conn, query) do
5 | {:ok, stmt} = Sqlite3.prepare(conn, query)
6 |
7 | Stream.repeatedly(fn -> Sqlite3.step(conn, stmt) end)
8 | |> Enum.take_while(fn
9 | :done -> false
10 | {:row, _} -> true
11 | end)
12 | |> Enum.map(fn {:row, values} ->
13 | values
14 | end)
15 | end
16 |
17 | def execute(conn, query, binds) when is_list(binds) and is_binary(query) do
18 | {:ok, stt} =
19 | Sqlite3.prepare(
20 | conn,
21 | query
22 | )
23 |
24 | :ok = Sqlite3.bind(stt, binds)
25 | :done = Sqlite3.step(conn, stt)
26 | :ok = Sqlite3.release(conn, stt)
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Boilerex.MixProject do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :boilerex,
7 | version: "0.1.0",
8 | elixir: "~> 1.18",
9 | start_permanent: Mix.env() == :prod,
10 | deps: deps(),
11 | compilers: [:temple] ++ Mix.compilers()
12 | ]
13 | end
14 |
15 | def application do
16 | [
17 | mod: {Boilerex.Application, []},
18 | extra_applications: [:logger]
19 | ]
20 | end
21 |
22 | defp deps do
23 | [
24 | {:plug_cowboy, "~> 2.6"},
25 | {:exsync, "~> 0.4", only: :dev},
26 | {:temple, "~> 0.14"},
27 | {:tailwind, "~> 0.3", only: :dev},
28 | {:exqlite, "~> 0.17"},
29 | {:html_entities, "~> 0.5"}
30 | ]
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/t.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/lib/web/views/pages/index.ex:
--------------------------------------------------------------------------------
1 | defmodule Web.Views.Pages.Index do
2 | alias Web.Views.Components.Task
3 | alias Web.Views.Layouts
4 | import Temple
5 | use Temple.Component
6 |
7 | def index_page(assigns) do
8 | temple do
9 | c &Layouts.Main.main_layout/1, title: "Index Page" do
10 | main class: "w-full flex flex-col gap-12 py-24 px-8 items-center" do
11 | h1 class: "text-4xl font-medium", do: "📝 Task Manager"
12 | c &Task.list/1, tasks: @tasks
13 |
14 | span class: "text-zinc-500" do
15 | "starterkit by "
16 |
17 | a href: "https://github.com/ethanhamilthon",
18 | target: "_blank",
19 | class: "hover:underline underline-none underline-white",
20 | do: "@ethanhamilthon"
21 | end
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/core/services/task.ex:
--------------------------------------------------------------------------------
1 | defmodule Core.Services.TaskService do
2 | alias Core.Repo.TaskRepo
3 |
4 | def list do
5 | TaskRepo.list()
6 | end
7 |
8 | def add(name) do
9 | tasks = TaskRepo.list()
10 |
11 | new_task =
12 | if length(tasks) == 0 do
13 | %{id: 1, name: name, done: false}
14 | else
15 | %{id: List.last(tasks).id + 1, name: name, done: false}
16 | end
17 |
18 | TaskRepo.create(new_task)
19 | tasks ++ [new_task]
20 | end
21 |
22 | def delete(id) do
23 | parsed_id = String.to_integer(id)
24 | TaskRepo.delete(parsed_id)
25 | TaskRepo.list()
26 | end
27 |
28 | def done(id) do
29 | parsed_id = String.to_integer(id)
30 | TaskRepo.update(parsed_id, true)
31 | TaskRepo.list()
32 | end
33 |
34 | def recover(id) do
35 | parsed_id = String.to_integer(id)
36 | TaskRepo.update(parsed_id, false)
37 | TaskRepo.list()
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/web/views/layouts/main.ex:
--------------------------------------------------------------------------------
1 | defmodule Web.Views.Layouts.Main do
2 | import Temple
3 | use Temple.Component
4 |
5 | def main_layout(assigns) do
6 | temple do
7 | ""
8 |
9 | html do
10 | head do
11 | meta charset: "utf-8"
12 | meta name: "viewport", content: "width=device-width, initial-scale=1.0"
13 | link rel: "stylesheet", href: "/assets/app.css"
14 | link rel: "icon", href: "/favicon.ico", type: "image/png"
15 |
16 | title do: @title
17 | end
18 |
19 | body do
20 | slot @inner_block
21 | end
22 |
23 | script src: "/assets/js/input_handler.js", defer: true
24 |
25 | script src: "https://unpkg.com/htmx.org@2.0.4",
26 | integrity:
27 | "sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+",
28 | crossorigin: "anonymous" do
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/web/router/main.ex:
--------------------------------------------------------------------------------
1 | defmodule Web.Router.Main do
2 | alias Core.Services.TaskService
3 | alias Web.Views.Components.Task
4 | alias Web.Views.Pages.Index
5 | use Plug.Router
6 | plug(Plug.Logger)
7 |
8 | plug(Plug.Static,
9 | at: "/",
10 | from: "priv/static",
11 | only: ~w(favicon.ico assets)
12 | )
13 |
14 | plug(Plug.Parsers,
15 | parsers: [:urlencoded, :multipart],
16 | pass: ["*/*"],
17 | length: 10_000_000
18 | )
19 |
20 | plug(:match)
21 | plug(:dispatch)
22 |
23 | get "/" do
24 | conn
25 | |> send_resp(200, Index.index_page(tasks: TaskService.list()))
26 | end
27 |
28 | post "/" do
29 | tasks = conn.params["name"] |> TaskService.add()
30 |
31 | conn
32 | |> send_resp(200, Task.list(tasks: tasks))
33 | end
34 |
35 | delete "/:id" do
36 | tasks = conn.params["id"] |> TaskService.delete()
37 |
38 | conn
39 | |> send_resp(200, Task.list(tasks: tasks))
40 | end
41 |
42 | patch "/:id" do
43 | tasks =
44 | case conn.query_params do
45 | %{"recover" => "true"} ->
46 | conn.params["id"] |> TaskService.recover()
47 |
48 | _ ->
49 | conn.params["id"] |> TaskService.done()
50 | end
51 |
52 | conn
53 | |> send_resp(200, Task.list(tasks: tasks))
54 | end
55 |
56 | match _ do
57 | send_resp(conn, 404, "Not found path")
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/lib/core/repo/task_repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Core.Repo.TaskRepo do
2 | alias Core.Repo.Helper
3 | alias Exqlite.Sqlite3
4 | alias Core.Repo.Connection
5 |
6 | defmodule TaskModel do
7 | defstruct [:id, :name, :done]
8 | end
9 |
10 | def list do
11 | Connection.get_conn()
12 | |> Helper.fetch_all_rows("select * from tasks")
13 | |> Enum.map(fn [id, name, done] ->
14 | %TaskModel{
15 | id: id,
16 | name: name,
17 | done:
18 | case done do
19 | "true" -> true
20 | "false" -> false
21 | end
22 | }
23 | end)
24 | end
25 |
26 | def create(id, name, done) do
27 | Connection.get_conn()
28 | |> Helper.execute(
29 | """
30 | insert into tasks (id, name, done) values (?,?,?)
31 | """,
32 | [id, name, done]
33 | )
34 | end
35 |
36 | def create(%{id: id, name: name, done: done}) do
37 | Connection.get_conn()
38 | |> Helper.execute(
39 | """
40 | insert into tasks (id, name, done) values (?,?,?)
41 | """,
42 | [id, name, done]
43 | )
44 | end
45 |
46 | def update(id, done) when is_boolean(done) do
47 | Connection.get_conn()
48 | |> Helper.execute("update tasks set done = ? where id = ?", [done, id])
49 | end
50 |
51 | def update(id, name, done) do
52 | Connection.get_conn()
53 | |> Helper.execute("update tasks set done = ?, name = ? where id = ?", [done, name, id])
54 | end
55 |
56 | def delete(id) do
57 | Connection.get_conn()
58 | |> Helper.execute("delete from tasks where id = ?", [id])
59 | end
60 |
61 | def migrate() do
62 | :ok =
63 | Connection.get_conn()
64 | |> Sqlite3.execute("""
65 | create table if not exists tasks (
66 | id int primary key,
67 | name text not null,
68 | done boolean not null
69 | );
70 | """)
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/lib/web/views/components/icons.ex:
--------------------------------------------------------------------------------
1 | defmodule Web.Views.Components.Icons do
2 | import Temple
3 | use Temple.Component
4 |
5 | def delete(assigns) do
6 | temple do
7 | svg width: 18,
8 | height: 18,
9 | viewbox: "0 0 15 15",
10 | fill: "none",
11 | xmlns: "http://www.w3.org/2000/svg",
12 | class: @class do
13 | path d:
14 | "M5.5 1C5.22386 1 5 1.22386 5 1.5C5 1.77614 5.22386 2 5.5 2H9.5C9.77614 2 10 1.77614 10 1.5C10 1.22386 9.77614 1 9.5 1H5.5ZM3 3.5C3 3.22386 3.22386 3 3.5 3H5H10H11.5C11.7761 3 12 3.22386 12 3.5C12 3.77614 11.7761 4 11.5 4H11V12C11 12.5523 10.5523 13 10 13H5C4.44772 13 4 12.5523 4 12V4L3.5 4C3.22386 4 3 3.77614 3 3.5ZM5 4H10V12H5V4Z",
15 | fill: "currentColor",
16 | fill_rule: "evenodd",
17 | clip_rule: "evenodd" do
18 | end
19 | end
20 | end
21 | end
22 |
23 | def check(assigns) do
24 | temple do
25 | svg class: @class,
26 | width: 18,
27 | height: 18,
28 | viewbox: "0 0 15 15",
29 | fill: "none",
30 | xmlns: "http://www.w3.org/2000/svg" do
31 | path d:
32 | "M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3355 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.55529 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z",
33 | fill: "currentColor",
34 | fill_rule: "evenodd",
35 | clip_rule: "evenodd" do
36 | end
37 | end
38 | end
39 | end
40 |
41 | def reset(assigns) do
42 | temple do
43 | svg class: @class,
44 | width: "15",
45 | height: "15",
46 | viewbox: "0 0 15 15",
47 | fill: "none",
48 | xmlns: "http://www.w3.org/2000/svg" do
49 | path d:
50 | "M4.85355 2.14645C5.04882 2.34171 5.04882 2.65829 4.85355 2.85355L3.70711 4H9C11.4853 4 13.5 6.01472 13.5 8.5C13.5 10.9853 11.4853 13 9 13H5C4.72386 13 4.5 12.7761 4.5 12.5C4.5 12.2239 4.72386 12 5 12H9C10.933 12 12.5 10.433 12.5 8.5C12.5 6.567 10.933 5 9 5H3.70711L4.85355 6.14645C5.04882 6.34171 5.04882 6.65829 4.85355 6.85355C4.65829 7.04882 4.34171 7.04882 4.14645 6.85355L2.14645 4.85355C1.95118 4.65829 1.95118 4.34171 2.14645 4.14645L4.14645 2.14645C4.34171 1.95118 4.65829 1.95118 4.85355 2.14645Z",
51 | fill: "currentColor",
52 | fill_rule: "evenodd",
53 | clip_rule: "evenodd" do
54 | end
55 | end
56 | end
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/lib/web/views/components/task.ex:
--------------------------------------------------------------------------------
1 | defmodule Web.Views.Components.Task do
2 | alias Web.Views.Components.Icons
3 | import Temple
4 | use Temple.Component
5 |
6 | def escape_tasks(data) when is_list(data) do
7 | {tasks, rest} = Keyword.pop(data, :tasks)
8 |
9 | Keyword.put(
10 | rest,
11 | :tasks,
12 | tasks |> Enum.map(fn t -> t |> Map.put(:name, HtmlEntities.encode(t.name)) end)
13 | )
14 | end
15 |
16 | def escape_tasks(data) do
17 | {tasks, rest} = Map.pop(data, :tasks)
18 |
19 | Map.put(
20 | rest,
21 | :tasks,
22 | tasks |> Enum.map(fn t -> t |> Map.put(:name, HtmlEntities.encode(t.name)) end)
23 | )
24 | end
25 |
26 | def list(assigns) do
27 | assigns = assigns |> escape_tasks()
28 | title_style = "text-xl font-medium text-zinc-300"
29 |
30 | temple do
31 | div id: "all-tasks",
32 | class:
33 | "w-full md:w-2/3 lg:w-1/2 flex flex-col gap-3 p-6 border border-zinc-800 rounded-xl" do
34 | span class: title_style do
35 | len = @tasks |> Enum.filter(fn e -> !e.done end) |> length()
36 |
37 | if len == 0 do
38 | "🔥 No tasks"
39 | else
40 | "🔥 You have #{len} #{if len > 1 do
41 | "things"
42 | else
43 | "thing"
44 | end} to do"
45 | end
46 | end
47 |
48 | for t <- @tasks do
49 | if !t.done do
50 | c &task_item/1, t: t, finish: false
51 | end
52 | end
53 |
54 | c &add_input/1
55 |
56 | if @tasks |> Enum.filter(fn e -> e.done end) |> length() != 0 do
57 | span class: title_style <> " mt-12", do: "✨ Finished tasks"
58 |
59 | for t <- @tasks do
60 | if t.done do
61 | c &task_item/1, t: t, finish: true
62 | end
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
69 | def task_item(assigns) do
70 | temple do
71 | div class: [
72 | "px-6 w-full py-3 rounded-md border border-zinc-600 flex justify-between items-center ",
73 | "line-through text-zinc-500 border-zinc-800": @finish
74 | ] do
75 | span class: "w-[90%]", do: @t.name
76 |
77 | div class: "flex gap-4 items-center" do
78 | if @finish do
79 | button "hx-swap": "outerHTML",
80 | "hx-target": "#all-tasks",
81 | "hx-patch": "/#{@t.id}?recover=true",
82 | "hx-trigger": "click",
83 | class: "cursor-pointer" do
84 | c &Icons.reset/1, class: "text-zinc-500 hover:text-purple-500 duration-300"
85 | end
86 | else
87 | button "hx-swap": "outerHTML",
88 | "hx-target": "#all-tasks",
89 | "hx-patch": "/#{@t.id}",
90 | "hx-trigger": "click",
91 | class: "cursor-pointer" do
92 | c &Icons.check/1, class: "text-white hover:text-green-500 duration-300"
93 | end
94 | end
95 |
96 | button "hx-swap": "outerHTML",
97 | "hx-target": "#all-tasks",
98 | "hx-delete": @t.id,
99 | "hx-trigger": "click",
100 | class: "cursor-pointer" do
101 | c &Icons.delete/1, class: "text-zinc-500 hover:text-red-500 duration-300"
102 | end
103 | end
104 | end
105 | end
106 | end
107 |
108 | def add_input(_) do
109 | temple do
110 | form class: "w-full relative",
111 | "hx-post": "/",
112 | "hx-swap": "outerHTML",
113 | "hx-target": "#all-tasks" do
114 | input id: "add-name-input",
115 | name: "name",
116 | type: "text",
117 | placeholder: "Title of the task",
118 | class:
119 | "w-full px-6 pr-20 py-3 placeholder:text-zinc-700 rounded-md border border-zinc-600 focus:outline-none"
120 |
121 | button type: "submit",
122 | class:
123 | "absolute top-1/2 cursor-pointer -translate-y-1/2 right-3 text-zinc-500 duration-300 hover:text-zinc-200",
124 | do: "Enter ⏎"
125 | end
126 |
127 | script do
128 | """
129 | document.getElementById("add-name-input").focus();
130 | """
131 | end
132 | end
133 | end
134 | end
135 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
3 | "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
4 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
5 | "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
6 | "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
7 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
8 | "exqlite": {:hex, :exqlite, "0.30.1", "a85ed253ab7304c3733a74d3bc62b68afb0c7245ce30416aa6f9d0cfece0e58f", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "15714871147d8d6c12be034013d351ce670e02c09b7f49accabb23e9290d80a0"},
9 | "exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},
10 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
11 | "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"},
12 | "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
13 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
14 | "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
15 | "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"},
16 | "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"},
17 | "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
18 | "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
19 | "tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
20 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
21 | "temple": {:hex, :temple, "0.14.1", "b6db3541bbc8eff7af630cc5949a47248c3ec46284275e06654f45c64e9437bf", [:mix], [{:floki, ">= 0.0.0", [hex: :floki, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "0bf9c7ebfda1bf5ab0621c9db0e6aad50b565513f22743529a03ed4b3d4baaae"},
22 | "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"},
23 | }
24 |
--------------------------------------------------------------------------------
/priv/static/assets/app.css:
--------------------------------------------------------------------------------
1 | /*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
2 | @layer theme, base, components, utilities;
3 | @layer theme {
4 | :root, :host {
5 | --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
6 | 'Noto Color Emoji';
7 | --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
8 | monospace;
9 | --color-red-500: oklch(0.637 0.237 25.331);
10 | --color-green-500: oklch(0.723 0.219 149.579);
11 | --color-purple-500: oklch(0.627 0.265 303.9);
12 | --color-zinc-200: oklch(0.92 0.004 286.32);
13 | --color-zinc-300: oklch(0.871 0.006 286.286);
14 | --color-zinc-500: oklch(0.552 0.016 285.938);
15 | --color-zinc-600: oklch(0.442 0.017 285.786);
16 | --color-zinc-700: oklch(0.37 0.013 285.805);
17 | --color-zinc-800: oklch(0.274 0.006 286.033);
18 | --color-zinc-900: oklch(0.21 0.006 285.885);
19 | --color-white: #fff;
20 | --spacing: 0.25rem;
21 | --text-xl: 1.25rem;
22 | --text-xl--line-height: calc(1.75 / 1.25);
23 | --text-2xl: 1.5rem;
24 | --text-2xl--line-height: calc(2 / 1.5);
25 | --text-4xl: 2.25rem;
26 | --text-4xl--line-height: calc(2.5 / 2.25);
27 | --font-weight-medium: 500;
28 | --radius-md: 0.375rem;
29 | --radius-xl: 0.75rem;
30 | --default-transition-duration: 150ms;
31 | --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
32 | --default-font-family: var(--font-sans);
33 | --default-font-feature-settings: var(--font-sans--font-feature-settings);
34 | --default-font-variation-settings: var(--font-sans--font-variation-settings);
35 | --default-mono-font-family: var(--font-mono);
36 | --default-mono-font-feature-settings: var(--font-mono--font-feature-settings);
37 | --default-mono-font-variation-settings: var(--font-mono--font-variation-settings);
38 | }
39 | }
40 | @layer base {
41 | *, ::after, ::before, ::backdrop, ::file-selector-button {
42 | box-sizing: border-box;
43 | margin: 0;
44 | padding: 0;
45 | border: 0 solid;
46 | }
47 | html, :host {
48 | line-height: 1.5;
49 | -webkit-text-size-adjust: 100%;
50 | tab-size: 4;
51 | font-family: var( --default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji' );
52 | font-feature-settings: var(--default-font-feature-settings, normal);
53 | font-variation-settings: var(--default-font-variation-settings, normal);
54 | -webkit-tap-highlight-color: transparent;
55 | }
56 | body {
57 | line-height: inherit;
58 | }
59 | hr {
60 | height: 0;
61 | color: inherit;
62 | border-top-width: 1px;
63 | }
64 | abbr:where([title]) {
65 | -webkit-text-decoration: underline dotted;
66 | text-decoration: underline dotted;
67 | }
68 | h1, h2, h3, h4, h5, h6 {
69 | font-size: inherit;
70 | font-weight: inherit;
71 | }
72 | a {
73 | color: inherit;
74 | -webkit-text-decoration: inherit;
75 | text-decoration: inherit;
76 | }
77 | b, strong {
78 | font-weight: bolder;
79 | }
80 | code, kbd, samp, pre {
81 | font-family: var( --default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace );
82 | font-feature-settings: var(--default-mono-font-feature-settings, normal);
83 | font-variation-settings: var(--default-mono-font-variation-settings, normal);
84 | font-size: 1em;
85 | }
86 | small {
87 | font-size: 80%;
88 | }
89 | sub, sup {
90 | font-size: 75%;
91 | line-height: 0;
92 | position: relative;
93 | vertical-align: baseline;
94 | }
95 | sub {
96 | bottom: -0.25em;
97 | }
98 | sup {
99 | top: -0.5em;
100 | }
101 | table {
102 | text-indent: 0;
103 | border-color: inherit;
104 | border-collapse: collapse;
105 | }
106 | :-moz-focusring {
107 | outline: auto;
108 | }
109 | progress {
110 | vertical-align: baseline;
111 | }
112 | summary {
113 | display: list-item;
114 | }
115 | ol, ul, menu {
116 | list-style: none;
117 | }
118 | img, svg, video, canvas, audio, iframe, embed, object {
119 | display: block;
120 | vertical-align: middle;
121 | }
122 | img, video {
123 | max-width: 100%;
124 | height: auto;
125 | }
126 | button, input, select, optgroup, textarea, ::file-selector-button {
127 | font: inherit;
128 | font-feature-settings: inherit;
129 | font-variation-settings: inherit;
130 | letter-spacing: inherit;
131 | color: inherit;
132 | border-radius: 0;
133 | background-color: transparent;
134 | opacity: 1;
135 | }
136 | :where(select:is([multiple], [size])) optgroup {
137 | font-weight: bolder;
138 | }
139 | :where(select:is([multiple], [size])) optgroup option {
140 | padding-inline-start: 20px;
141 | }
142 | ::file-selector-button {
143 | margin-inline-end: 4px;
144 | }
145 | ::placeholder {
146 | opacity: 1;
147 | color: color-mix(in oklab, currentColor 50%, transparent);
148 | }
149 | textarea {
150 | resize: vertical;
151 | }
152 | ::-webkit-search-decoration {
153 | -webkit-appearance: none;
154 | }
155 | ::-webkit-date-and-time-value {
156 | min-height: 1lh;
157 | text-align: inherit;
158 | }
159 | ::-webkit-datetime-edit {
160 | display: inline-flex;
161 | }
162 | ::-webkit-datetime-edit-fields-wrapper {
163 | padding: 0;
164 | }
165 | ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
166 | padding-block: 0;
167 | }
168 | :-moz-ui-invalid {
169 | box-shadow: none;
170 | }
171 | button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button {
172 | appearance: button;
173 | }
174 | ::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
175 | height: auto;
176 | }
177 | [hidden]:where(:not([hidden='until-found'])) {
178 | display: none !important;
179 | }
180 | }
181 | @layer utilities {
182 | .collapse {
183 | visibility: collapse;
184 | }
185 | .visible {
186 | visibility: visible;
187 | }
188 | .absolute {
189 | position: absolute;
190 | }
191 | .fixed {
192 | position: fixed;
193 | }
194 | .relative {
195 | position: relative;
196 | }
197 | .static {
198 | position: static;
199 | }
200 | .top-1 {
201 | top: calc(var(--spacing) * 1);
202 | }
203 | .top-1\/2 {
204 | top: calc(1/2 * 100%);
205 | }
206 | .right-3 {
207 | right: calc(var(--spacing) * 3);
208 | }
209 | .z-1 {
210 | z-index: 1;
211 | }
212 | .container {
213 | width: 100%;
214 | @media (width >= 40rem) {
215 | max-width: 40rem;
216 | }
217 | @media (width >= 48rem) {
218 | max-width: 48rem;
219 | }
220 | @media (width >= 64rem) {
221 | max-width: 64rem;
222 | }
223 | @media (width >= 80rem) {
224 | max-width: 80rem;
225 | }
226 | @media (width >= 96rem) {
227 | max-width: 96rem;
228 | }
229 | }
230 | .m-0 {
231 | margin: calc(var(--spacing) * 0);
232 | }
233 | .m-1 {
234 | margin: calc(var(--spacing) * 1);
235 | }
236 | .m-6 {
237 | margin: calc(var(--spacing) * 6);
238 | }
239 | .mt-12 {
240 | margin-top: calc(var(--spacing) * 12);
241 | }
242 | .\!hidden {
243 | display: none !important;
244 | }
245 | .block {
246 | display: block;
247 | }
248 | .contents {
249 | display: contents;
250 | }
251 | .flex {
252 | display: flex;
253 | }
254 | .hidden {
255 | display: none;
256 | }
257 | .inline {
258 | display: inline;
259 | }
260 | .inline-block {
261 | display: inline-block;
262 | }
263 | .inline-flex {
264 | display: inline-flex;
265 | }
266 | .list-item {
267 | display: list-item;
268 | }
269 | .table {
270 | display: table;
271 | }
272 | .size-1 {
273 | width: calc(var(--spacing) * 1);
274 | height: calc(var(--spacing) * 1);
275 | }
276 | .size-2 {
277 | width: calc(var(--spacing) * 2);
278 | height: calc(var(--spacing) * 2);
279 | }
280 | .w-0 {
281 | width: calc(var(--spacing) * 0);
282 | }
283 | .w-1 {
284 | width: calc(var(--spacing) * 1);
285 | }
286 | .w-1\/2 {
287 | width: calc(1/2 * 100%);
288 | }
289 | .w-2\/3 {
290 | width: calc(2/3 * 100%);
291 | }
292 | .w-5 {
293 | width: calc(var(--spacing) * 5);
294 | }
295 | .w-7 {
296 | width: calc(var(--spacing) * 7);
297 | }
298 | .w-\[90\%\] {
299 | width: 90%;
300 | }
301 | .w-full {
302 | width: 100%;
303 | }
304 | .shrink {
305 | flex-shrink: 1;
306 | }
307 | .flex-grow {
308 | flex-grow: 1;
309 | }
310 | .grow {
311 | flex-grow: 1;
312 | }
313 | .border-collapse {
314 | border-collapse: collapse;
315 | }
316 | .-translate-y-1 {
317 | --tw-translate-y: calc(var(--spacing) * -1);
318 | translate: var(--tw-translate-x) var(--tw-translate-y);
319 | }
320 | .-translate-y-1\/2 {
321 | --tw-translate-y: calc(calc(1/2 * 100%) * -1);
322 | translate: var(--tw-translate-x) var(--tw-translate-y);
323 | }
324 | .transform {
325 | transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
326 | }
327 | .cursor-pointer {
328 | cursor: pointer;
329 | }
330 | .resize {
331 | resize: both;
332 | }
333 | .flex-col {
334 | flex-direction: column;
335 | }
336 | .items-center {
337 | align-items: center;
338 | }
339 | .justify-between {
340 | justify-content: space-between;
341 | }
342 | .gap-3 {
343 | gap: calc(var(--spacing) * 3);
344 | }
345 | .gap-4 {
346 | gap: calc(var(--spacing) * 4);
347 | }
348 | .gap-12 {
349 | gap: calc(var(--spacing) * 12);
350 | }
351 | .truncate {
352 | overflow: hidden;
353 | text-overflow: ellipsis;
354 | white-space: nowrap;
355 | }
356 | .rounded {
357 | border-radius: 0.25rem;
358 | }
359 | .rounded-md {
360 | border-radius: var(--radius-md);
361 | }
362 | .rounded-xl {
363 | border-radius: var(--radius-xl);
364 | }
365 | .border {
366 | border-style: var(--tw-border-style);
367 | border-width: 1px;
368 | }
369 | .border-zinc-600 {
370 | border-color: var(--color-zinc-600);
371 | }
372 | .border-zinc-800 {
373 | border-color: var(--color-zinc-800);
374 | }
375 | .p-6 {
376 | padding: calc(var(--spacing) * 6);
377 | }
378 | .p-8 {
379 | padding: calc(var(--spacing) * 8);
380 | }
381 | .px-6 {
382 | padding-inline: calc(var(--spacing) * 6);
383 | }
384 | .px-8 {
385 | padding-inline: calc(var(--spacing) * 8);
386 | }
387 | .py-3 {
388 | padding-block: calc(var(--spacing) * 3);
389 | }
390 | .py-24 {
391 | padding-block: calc(var(--spacing) * 24);
392 | }
393 | .pr-16 {
394 | padding-right: calc(var(--spacing) * 16);
395 | }
396 | .pr-20 {
397 | padding-right: calc(var(--spacing) * 20);
398 | }
399 | .text-2xl {
400 | font-size: var(--text-2xl);
401 | line-height: var(--tw-leading, var(--text-2xl--line-height));
402 | }
403 | .text-4xl {
404 | font-size: var(--text-4xl);
405 | line-height: var(--tw-leading, var(--text-4xl--line-height));
406 | }
407 | .text-xl {
408 | font-size: var(--text-xl);
409 | line-height: var(--tw-leading, var(--text-xl--line-height));
410 | }
411 | .font-medium {
412 | --tw-font-weight: var(--font-weight-medium);
413 | font-weight: var(--font-weight-medium);
414 | }
415 | .text-white {
416 | color: var(--color-white);
417 | }
418 | .text-zinc-300 {
419 | color: var(--color-zinc-300);
420 | }
421 | .text-zinc-500 {
422 | color: var(--color-zinc-500);
423 | }
424 | .lowercase {
425 | text-transform: lowercase;
426 | }
427 | .uppercase {
428 | text-transform: uppercase;
429 | }
430 | .line-through {
431 | text-decoration-line: line-through;
432 | }
433 | .underline {
434 | text-decoration-line: underline;
435 | }
436 | .shadow {
437 | --tw-shadow: 0 1px 3px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 1px 2px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
438 | box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
439 | }
440 | .ring {
441 | --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
442 | box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
443 | }
444 | .outline {
445 | outline-style: var(--tw-outline-style);
446 | outline-width: 1px;
447 | }
448 | .invert {
449 | --tw-invert: invert(100%);
450 | filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
451 | }
452 | .filter {
453 | filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
454 | }
455 | .backdrop-filter {
456 | -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
457 | backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);
458 | }
459 | .transition {
460 | transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
461 | transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
462 | transition-duration: var(--tw-duration, var(--default-transition-duration));
463 | }
464 | .duration-300 {
465 | --tw-duration: 300ms;
466 | transition-duration: 300ms;
467 | }
468 | .\[c\:lm\(\)\] {
469 | c: lm();
470 | }
471 | .\[code\:lib_dir\(erts\)\] {
472 | code: lib dir(erts);
473 | }
474 | .\[cow_http2\:streamid\(\)\] {
475 | cow_http2: streamid();
476 | }
477 | .\[cow_link\:link\(\)\] {
478 | cow_link: link();
479 | }
480 | .\[cow_sse\:event\(\)\] {
481 | cow_sse: event();
482 | }
483 | .\[cow_ws\:frame\(\)\] {
484 | cow_ws: frame();
485 | }
486 | .\[cowboy_constraints\:constraint\(\)\] {
487 | cowboy_constraints: constraint();
488 | }
489 | .\[dateof\:3\.0\.0\] {
490 | dateof: 3.0.0;
491 | }
492 | .\[dateof\:3\.3\.0\] {
493 | dateof: 3.3.0;
494 | }
495 | .\[dateof\:3\.3\.8\] {
496 | dateof: 3.3.8;
497 | }
498 | .\[dateof\:3\.5\.0\] {
499 | dateof: 3.5.0;
500 | }
501 | .\[dateof\:3\.6\.0\] {
502 | dateof: 3.6.0;
503 | }
504 | .\[dateof\:3\.6\.18\] {
505 | dateof: 3.6.18;
506 | }
507 | .\[dateof\:3\.6\.23\.1\] {
508 | dateof: 3.6.23.1;
509 | }
510 | .\[dateof\:3\.7\.0\] {
511 | dateof: 3.7.0;
512 | }
513 | .\[dateof\:3\.7\.6\] {
514 | dateof: 3.7.6;
515 | }
516 | .\[dateof\:3\.8\.2\] {
517 | dateof: 3.8.2;
518 | }
519 | .\[dateof\:3\.9\.0\] {
520 | dateof: 3.9.0;
521 | }
522 | .\[dateof\:3\.31\.0\] {
523 | dateof: 3.31.0;
524 | }
525 | .\[dateof\:3\.44\.0\] {
526 | dateof: 3.44.0;
527 | }
528 | .\[lists\:reverse\(Buffer\)\|Acc\] {
529 | lists: reverse(Buffer)|Acc;
530 | }
531 | .\[public_key\:der_encoded\(\)\] {
532 | public_key: der encoded();
533 | }
534 | .\[ssl\:group\(\)\] {
535 | ssl: group();
536 | }
537 | .\[ssl\:named_curve\(\)\] {
538 | ssl: named curve();
539 | }
540 | .\[ssl\:protocol_version\(\)\] {
541 | ssl: protocol version();
542 | }
543 | .\[ssl\:sign_scheme\(\)\] {
544 | ssl: sign scheme();
545 | }
546 | .\[ssl\:tls_server_option\(\)\] {
547 | ssl: tls server option();
548 | }
549 | .\[supervisor\:child_spec\(\)\] {
550 | supervisor: child spec();
551 | }
552 | .\[telemetry\:event_name\(\)\] {
553 | telemetry: event name();
554 | }
555 | .placeholder\:text-zinc-700 {
556 | &::placeholder {
557 | color: var(--color-zinc-700);
558 | }
559 | }
560 | .hover\:text-green-500 {
561 | &:hover {
562 | @media (hover: hover) {
563 | color: var(--color-green-500);
564 | }
565 | }
566 | }
567 | .hover\:text-purple-500 {
568 | &:hover {
569 | @media (hover: hover) {
570 | color: var(--color-purple-500);
571 | }
572 | }
573 | }
574 | .hover\:text-red-500 {
575 | &:hover {
576 | @media (hover: hover) {
577 | color: var(--color-red-500);
578 | }
579 | }
580 | }
581 | .hover\:text-zinc-200 {
582 | &:hover {
583 | @media (hover: hover) {
584 | color: var(--color-zinc-200);
585 | }
586 | }
587 | }
588 | .hover\:underline {
589 | &:hover {
590 | @media (hover: hover) {
591 | text-decoration-line: underline;
592 | }
593 | }
594 | }
595 | .focus\:outline-none {
596 | &:focus {
597 | --tw-outline-style: none;
598 | outline-style: none;
599 | }
600 | }
601 | .md\:w-2\/3 {
602 | @media (width >= 48rem) {
603 | width: calc(2/3 * 100%);
604 | }
605 | }
606 | .lg\:w-1\/2 {
607 | @media (width >= 64rem) {
608 | width: calc(1/2 * 100%);
609 | }
610 | }
611 | }
612 | :root {
613 | background-color: var(--color-zinc-900);
614 | color: var(--color-white);
615 | }
616 | @property --tw-translate-x {
617 | syntax: "*";
618 | inherits: false;
619 | initial-value: 0;
620 | }
621 | @property --tw-translate-y {
622 | syntax: "*";
623 | inherits: false;
624 | initial-value: 0;
625 | }
626 | @property --tw-translate-z {
627 | syntax: "*";
628 | inherits: false;
629 | initial-value: 0;
630 | }
631 | @property --tw-rotate-x {
632 | syntax: "*";
633 | inherits: false;
634 | initial-value: rotateX(0);
635 | }
636 | @property --tw-rotate-y {
637 | syntax: "*";
638 | inherits: false;
639 | initial-value: rotateY(0);
640 | }
641 | @property --tw-rotate-z {
642 | syntax: "*";
643 | inherits: false;
644 | initial-value: rotateZ(0);
645 | }
646 | @property --tw-skew-x {
647 | syntax: "*";
648 | inherits: false;
649 | initial-value: skewX(0);
650 | }
651 | @property --tw-skew-y {
652 | syntax: "*";
653 | inherits: false;
654 | initial-value: skewY(0);
655 | }
656 | @property --tw-border-style {
657 | syntax: "*";
658 | inherits: false;
659 | initial-value: solid;
660 | }
661 | @property --tw-font-weight {
662 | syntax: "*";
663 | inherits: false;
664 | }
665 | @property --tw-shadow {
666 | syntax: "*";
667 | inherits: false;
668 | initial-value: 0 0 #0000;
669 | }
670 | @property --tw-shadow-color {
671 | syntax: "*";
672 | inherits: false;
673 | }
674 | @property --tw-inset-shadow {
675 | syntax: "*";
676 | inherits: false;
677 | initial-value: 0 0 #0000;
678 | }
679 | @property --tw-inset-shadow-color {
680 | syntax: "*";
681 | inherits: false;
682 | }
683 | @property --tw-ring-color {
684 | syntax: "*";
685 | inherits: false;
686 | }
687 | @property --tw-ring-shadow {
688 | syntax: "*";
689 | inherits: false;
690 | initial-value: 0 0 #0000;
691 | }
692 | @property --tw-inset-ring-color {
693 | syntax: "*";
694 | inherits: false;
695 | }
696 | @property --tw-inset-ring-shadow {
697 | syntax: "*";
698 | inherits: false;
699 | initial-value: 0 0 #0000;
700 | }
701 | @property --tw-ring-inset {
702 | syntax: "*";
703 | inherits: false;
704 | }
705 | @property --tw-ring-offset-width {
706 | syntax: "";
707 | inherits: false;
708 | initial-value: 0px;
709 | }
710 | @property --tw-ring-offset-color {
711 | syntax: "*";
712 | inherits: false;
713 | initial-value: #fff;
714 | }
715 | @property --tw-ring-offset-shadow {
716 | syntax: "*";
717 | inherits: false;
718 | initial-value: 0 0 #0000;
719 | }
720 | @property --tw-outline-style {
721 | syntax: "*";
722 | inherits: false;
723 | initial-value: solid;
724 | }
725 | @property --tw-blur {
726 | syntax: "*";
727 | inherits: false;
728 | }
729 | @property --tw-brightness {
730 | syntax: "*";
731 | inherits: false;
732 | }
733 | @property --tw-contrast {
734 | syntax: "*";
735 | inherits: false;
736 | }
737 | @property --tw-grayscale {
738 | syntax: "*";
739 | inherits: false;
740 | }
741 | @property --tw-hue-rotate {
742 | syntax: "*";
743 | inherits: false;
744 | }
745 | @property --tw-invert {
746 | syntax: "*";
747 | inherits: false;
748 | }
749 | @property --tw-opacity {
750 | syntax: "*";
751 | inherits: false;
752 | }
753 | @property --tw-saturate {
754 | syntax: "*";
755 | inherits: false;
756 | }
757 | @property --tw-sepia {
758 | syntax: "*";
759 | inherits: false;
760 | }
761 | @property --tw-drop-shadow {
762 | syntax: "*";
763 | inherits: false;
764 | }
765 | @property --tw-backdrop-blur {
766 | syntax: "*";
767 | inherits: false;
768 | }
769 | @property --tw-backdrop-brightness {
770 | syntax: "*";
771 | inherits: false;
772 | }
773 | @property --tw-backdrop-contrast {
774 | syntax: "*";
775 | inherits: false;
776 | }
777 | @property --tw-backdrop-grayscale {
778 | syntax: "*";
779 | inherits: false;
780 | }
781 | @property --tw-backdrop-hue-rotate {
782 | syntax: "*";
783 | inherits: false;
784 | }
785 | @property --tw-backdrop-invert {
786 | syntax: "*";
787 | inherits: false;
788 | }
789 | @property --tw-backdrop-opacity {
790 | syntax: "*";
791 | inherits: false;
792 | }
793 | @property --tw-backdrop-saturate {
794 | syntax: "*";
795 | inherits: false;
796 | }
797 | @property --tw-backdrop-sepia {
798 | syntax: "*";
799 | inherits: false;
800 | }
801 | @property --tw-duration {
802 | syntax: "*";
803 | inherits: false;
804 | }
805 |
--------------------------------------------------------------------------------