├── 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 | 8 | 14 | 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 | --------------------------------------------------------------------------------