├── test ├── test_helper.exs └── phexel_test.exs ├── .tool-versions ├── .formatter.exs ├── lib ├── phexel │ ├── icon.ex │ ├── container.ex │ ├── center.ex │ ├── frame.ex │ ├── stack.ex │ ├── grid.ex │ ├── switcher.ex │ ├── imposter.ex │ ├── box.ex │ ├── reel.ex │ ├── cover.ex │ ├── cluster.ex │ └── sidebar.ex └── phexel.ex ├── mix.exs ├── .gitignore ├── .devcontainer ├── devcontainer.json ├── docker-compose.yml └── Dockerfile ├── README.md ├── mix.lock └── assets └── elc.css /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.16.3-otp-26 2 | erlang 26.2.5 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | plugins: [Styler], 4 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 5 | ] 6 | -------------------------------------------------------------------------------- /lib/phexel/icon.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Icon do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [] 8 | 9 | attr(:tag, :string, default: "i") 10 | attr(:rest, :global) 11 | 12 | slot(:inner_block, required: false) 13 | 14 | def icon(assigns), do: base(assigns, @allowed_configuration_keys, "elc-icon") 15 | end 16 | -------------------------------------------------------------------------------- /lib/phexel/container.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Container do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"container-name" 9 | ] 10 | 11 | attr(:"container-name", :string, required: true) 12 | attr(:tag, :string, default: "div") 13 | attr(:rest, :global) 14 | 15 | slot(:inner_block, required: true) 16 | 17 | def container(assigns), do: base(assigns, @allowed_configuration_keys, "elc-container") 18 | end 19 | -------------------------------------------------------------------------------- /lib/phexel/center.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Center do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"center-padding-inline" 9 | ] 10 | 11 | attr(:"center-padding-inline", :string, required: false) 12 | attr(:tag, :string, default: "div") 13 | attr(:rest, :global) 14 | 15 | slot(:inner_block, required: true) 16 | 17 | def center(assigns), do: base(assigns, @allowed_configuration_keys, "elc-center") 18 | end 19 | -------------------------------------------------------------------------------- /lib/phexel/frame.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Frame do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"frame-width", 9 | :"frame-height" 10 | ] 11 | 12 | attr(:"frame-width", :string) 13 | attr(:"frame-height", :string) 14 | attr(:tag, :string, default: "div") 15 | attr(:rest, :global) 16 | 17 | slot(:inner_block, required: true) 18 | 19 | def frame(assigns), do: base(assigns, @allowed_configuration_keys, "elc-frame") 20 | end 21 | -------------------------------------------------------------------------------- /lib/phexel/stack.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Stack do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"stack-margin", 9 | :"stack-split" 10 | ] 11 | 12 | attr(:"stack-margin", :string) 13 | attr(:"stack-split", :string) 14 | attr(:tag, :string, default: "div") 15 | attr(:rest, :global) 16 | 17 | slot(:inner_block, required: true) 18 | 19 | def stack(assigns), do: base(assigns, @allowed_configuration_keys, "elc-stack") 20 | end 21 | -------------------------------------------------------------------------------- /lib/phexel/grid.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Grid do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"grid-grid-gap", 9 | :"grid-column-min-width" 10 | ] 11 | 12 | attr(:"grid-grid-gap", :string) 13 | attr(:"grid-column-min-width", :string) 14 | attr(:tag, :string, default: "div") 15 | attr(:rest, :global) 16 | 17 | slot(:inner_block, required: true) 18 | 19 | def grid(assigns), do: base(assigns, @allowed_configuration_keys, "elc-grid") 20 | end 21 | -------------------------------------------------------------------------------- /lib/phexel/switcher.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Switcher do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"switcher-gap", 9 | :"switcher-threshold" 10 | ] 11 | 12 | attr(:"switcher-gap", :string) 13 | attr(:"switcher-threshold", :string) 14 | attr(:tag, :string, default: "div") 15 | attr(:rest, :global) 16 | 17 | slot(:inner_block, required: true) 18 | 19 | def switcher(assigns), do: base(assigns, @allowed_configuration_keys, "elc-switcher") 20 | end 21 | -------------------------------------------------------------------------------- /lib/phexel/imposter.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Imposter do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"imposter-position", 9 | :"imposter-margin" 10 | ] 11 | 12 | attr(:"imposter-position", :string) 13 | attr(:"imposter-margin", :string) 14 | attr(:tag, :string, default: "div") 15 | attr(:rest, :global) 16 | 17 | slot(:inner_block, required: true) 18 | 19 | def imposter(assigns), do: base(assigns, @allowed_configuration_keys, "elc-imposter") 20 | end 21 | -------------------------------------------------------------------------------- /lib/phexel/box.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Box do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"box-padding", 9 | :"box-border-width" 10 | ] 11 | 12 | attr(:"box-padding", :string, required: false) 13 | attr(:"box-border-width", :string, required: false) 14 | attr(:tag, :string, default: "div") 15 | attr(:rest, :global) 16 | 17 | slot(:inner_block, required: true) 18 | 19 | def box(assigns), do: base(assigns, @allowed_configuration_keys, "elc-box") 20 | end 21 | -------------------------------------------------------------------------------- /lib/phexel/reel.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Reel do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"reel-block-size", 9 | :"reel-padding", 10 | :"reel-gap" 11 | ] 12 | 13 | attr(:"reel-block-size", :string) 14 | attr(:"reel-padding", :string) 15 | attr(:"reel-gap", :string) 16 | attr(:tag, :string, default: "div") 17 | attr(:rest, :global) 18 | 19 | slot(:inner_block, required: true) 20 | 21 | def reel(assigns), do: base(assigns, @allowed_configuration_keys, "elc-reel") 22 | end 23 | -------------------------------------------------------------------------------- /lib/phexel/cover.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Cover do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"cover-padding", 9 | :"cover-margin", 10 | :"cover-min-block-size" 11 | ] 12 | 13 | attr(:"cover-padding", :string) 14 | attr(:"cover-margin", :string) 15 | attr(:"cover-min-block-size", :string) 16 | attr(:tag, :string, default: "div") 17 | attr(:rest, :global) 18 | 19 | slot(:inner_block, required: true) 20 | 21 | def cover(assigns), do: base(assigns, @allowed_configuration_keys, "elc-cover") 22 | end 23 | -------------------------------------------------------------------------------- /lib/phexel/cluster.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Cluster do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"cluster-gap", 9 | :"cluster-justify-content", 10 | :"cluster-align-items" 11 | ] 12 | 13 | attr(:"cluster-gap", :string) 14 | attr(:"cluster-justify-content", :string) 15 | attr(:"cluster-align-items", :string) 16 | attr(:tag, :string, default: "div") 17 | attr(:rest, :global) 18 | 19 | slot(:inner_block, required: true) 20 | 21 | def cluster(assigns), do: base(assigns, @allowed_configuration_keys, "elc-cluster") 22 | end 23 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Phexel.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :phexel, 7 | version: "0.1.0", 8 | elixir: "~> 1.13", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:phoenix_live_view, "~> 0.20"}, 25 | {:styler, "~> 1.0.0-rc.0", only: [:dev, :test], runtime: false} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/phexel/sidebar.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel.Sidebar do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | import Phexel, only: [base: 3] 6 | 7 | @allowed_configuration_keys [ 8 | :"sidebar-flex-basis", 9 | :"sidebar-min-inline-size", 10 | :"sidebar-gap", 11 | :"sidebar-flex-direction" 12 | ] 13 | 14 | attr(:"sidebar-flex-basis", :string) 15 | attr(:"sidebar-min-inline-size", :string) 16 | attr(:"sidebar-gap", :string) 17 | attr(:"sidebar-flex-direction", :string) 18 | attr(:tag, :string, default: "div") 19 | attr(:rest, :global) 20 | 21 | slot(:inner_block, required: true) 22 | 23 | def sidebar(assigns), do: base(assigns, @allowed_configuration_keys, "elc-sidebar") 24 | end 25 | -------------------------------------------------------------------------------- /.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 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | phexel-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Development Configuration", 3 | "dockerComposeFile": [ 4 | // In case you want to startup some infrastructure service 5 | // "../docker-compose.yml", 6 | "docker-compose.yml" 7 | ], 8 | "service": "development", 9 | "workspaceFolder": "/workspace", 10 | "remoteUser": "gitpod", 11 | // In case anything is supposed to run once, initially. 12 | // "onCreateCommand": "bash -i -c 'mix assets.setup'", 13 | // "updateContentCommand": "bash -i -c 'direnv allow && mix do deps.get, compile --force && MIX_ENV=test mix compile --force'", 14 | "customizations": { 15 | "vscode": { 16 | "extensions": [ 17 | "JakeBecker.elixir-ls", 18 | "phoenixframework.phoenix", 19 | "me-dutour-mathieu.vscode-github-actions", 20 | "eamodio.gitlens", 21 | "ms-azuretools.vscode-docker", 22 | "bungcip.better-toml" 23 | ], 24 | "settings": { 25 | "gitlens": { 26 | "showWelcomeOnInstall": false, 27 | "showWhatsNewAfterUpgrades": false 28 | } 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /test/phexel_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhexelTest do 2 | use ExUnit.Case 3 | 4 | doctest Phexel 5 | 6 | describe "Has configuration to parse" do 7 | test "correctly parses configuration" do 8 | allowed_configuration_keys = [:"my-setting"] 9 | assigns = %{"my-setting": "cool-value"} 10 | 11 | assert "--my-setting: var(--cool-value, cool-value);" = 12 | Phexel.build_configuration_style(assigns, allowed_configuration_keys) 13 | end 14 | 15 | test "ignores unkown keys" do 16 | allowed_configuration_keys = [] 17 | assigns = %{"my-setting": "cool-value"} 18 | 19 | assert "" = Phexel.build_configuration_style(assigns, allowed_configuration_keys) 20 | end 21 | 22 | test "handles literal styles" do 23 | allowed_configuration_keys = [:"my-setting"] 24 | assigns = %{"my-setting": "cool-value", rest: %{style: "display: flex;"}} 25 | 26 | assert "--my-setting: var(--cool-value, cool-value); display: flex;" = 27 | Phexel.build_configuration_style(assigns, allowed_configuration_keys) 28 | end 29 | end 30 | 31 | describe "Correctly adds classes" do 32 | test "no additional classes" do 33 | assigns = %{"my-setting": "cool-value"} 34 | 35 | assert "my-class" = Phexel.build_configuration_class(assigns, "my-class") 36 | end 37 | 38 | test "with additional classes" do 39 | assigns = %{rest: %{class: "foo"}} 40 | 41 | assert "my-class foo" = Phexel.build_configuration_class(assigns, "my-class") 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | # Update this to the name of the service you want to work with in your docker-compose.yml file 4 | development: 5 | environment: 6 | # dev config 7 | - GIT_EDITOR=code --wait 8 | 9 | # If you want add a non-root user to your Dockerfile, you can use the "remoteUser" 10 | # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, 11 | # debugging) to execute as the user. Uncomment the next line if you want the entire 12 | # container to run as this user instead. Note that, on Linux, you may need to 13 | # ensure the UID and GID of the container user you create matches your local user. 14 | # See https://aka.ms/vscode-remote/containers/non-root for details. 15 | # 16 | # user: vscode 17 | 18 | # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer 19 | # folder. Note that the path of the Dockerfile and context is relative to the *primary* 20 | # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" 21 | # array). The sample below assumes your primary file is in the root of your project. 22 | # 23 | build: 24 | context: ../ 25 | target: development 26 | dockerfile: .devcontainer/Dockerfile 27 | 28 | volumes: 29 | # Update this to wherever you want VS Code to mount the folder of your project 30 | - ..:/workspace:cached 31 | # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details. 32 | # - /var/run/docker.sock:/var/run/docker.sock 33 | 34 | # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. 35 | # cap_add: 36 | # - SYS_PTRACE 37 | # security_opt: 38 | # - seccomp:unconfined 39 | 40 | # Overrides default command so things don't shut down after the process ends. 41 | command: sleep infinity -------------------------------------------------------------------------------- /lib/phexel.ex: -------------------------------------------------------------------------------- 1 | defmodule Phexel do 2 | @moduledoc """ 3 | This module contains layout components as they are described here: https://every-layout.dev 4 | The corresponding styles are here: https://elc.silvan.codes/elc.css 5 | """ 6 | 7 | import Phoenix.Component, only: [sigil_H: 2, render_slot: 1, dynamic_tag: 1, assign: 2] 8 | 9 | @spec base(map(), keyword(), String.t()) :: Phoenix.LiveView.Rendered.t() 10 | def base(assigns, allowed_configuration_keys, type) do 11 | assigns = 12 | assign(assigns, 13 | rest: 14 | assigns.rest 15 | |> Map.put(:style, build_configuration_style(assigns, allowed_configuration_keys)) 16 | |> Map.put(:class, build_configuration_class(assigns, type)) 17 | ) 18 | 19 | ~H""" 20 | <.dynamic_tag name={@tag} {@rest}> 21 | <%= render_slot(@inner_block) %> 22 | 23 | """ 24 | end 25 | 26 | defdelegate stack(assigns), to: Phexel.Stack 27 | defdelegate box(assigns), to: Phexel.Box 28 | defdelegate center(assigns), to: Phexel.Center 29 | defdelegate cluster(assigns), to: Phexel.Cluster 30 | defdelegate cover(assigns), to: Phexel.Cover 31 | defdelegate frame(assigns), to: Phexel.Frame 32 | defdelegate grid(assigns), to: Phexel.Grid 33 | defdelegate imposter(assigns), to: Phexel.Imposter 34 | defdelegate reel(assigns), to: Phexel.Reel 35 | defdelegate sidebar(assigns), to: Phexel.Sidebar 36 | defdelegate switcher(assigns), to: Phexel.Switcher 37 | defdelegate container(assigns), to: Phexel.Container 38 | defdelegate icon(assigns), to: Phexel.Icon 39 | 40 | def build_configuration_style(assigns, keys) when is_list(keys) do 41 | [extract_configuration_styles(assigns, keys), extract_literal_styles(assigns)] 42 | |> Enum.join(" ") 43 | |> String.trim() 44 | end 45 | 46 | def build_configuration_class(assigns, class) do 47 | [class, extract_literal_classes(assigns)] |> Enum.join(" ") |> String.trim() 48 | end 49 | 50 | defp extract_configuration_styles(assigns, keys) do 51 | for {key, value} <- assigns, 52 | key in keys, 53 | into: "", 54 | do: "--#{key}: #{var(value)};" 55 | end 56 | 57 | defp var(value) do 58 | "var(--#{value}, #{value})" 59 | end 60 | 61 | defp extract_literal_styles(%{rest: %{style: literal_styles}}) do 62 | literal_styles 63 | end 64 | 65 | defp extract_literal_styles(%{}), do: "" 66 | 67 | defp extract_literal_classes(%{rest: %{class: literal_class}}) do 68 | literal_class 69 | end 70 | 71 | defp extract_literal_classes(%{}), do: "" 72 | end 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phexel 2 | 3 | Phexel brings [Every Layout](https://every-layout.dev) to Phoenix! 4 | 5 | If you think it's good, consider buying yourself a copy. 6 | 7 | ## Installation 8 | 9 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 10 | by adding `phexel` to your list of dependencies in `mix.exs`: 11 | 12 | ```elixir 13 | def deps do 14 | [ 15 | {:phexel, "~> 0.1.0"} 16 | ] 17 | end 18 | ``` 19 | otherwise go this way: 20 | 21 | ```elixir 22 | def deps do 23 | [ 24 | {:phexel, git: "https://github.com/SilvanCodes/phexel"} 25 | ] 26 | end 27 | ``` 28 | 29 | Assuming a standard Phoenix project structure, add the following line to your `app.css`: 30 | ```css 31 | @import "../../deps/phexel/assets/elc.css"; 32 | ``` 33 | 34 | ## Usage 35 | 36 | Phexel provides a `Phoenix.Component` for each layout from [Every Layout](https://every-layout.dev). You can use these components by importing `Phexel` into your LiveView or LiveComponent like this: 37 | 38 | ```elixir 39 | defmodule Web.Component.TodoList do 40 | use Phoenix.LiveComponent 41 | import Phexel 42 | 43 | def todo_list(assigns) do 44 | ~H""" 45 | <.box> 46 | <.stack> 47 |
48 | <%= for todo <- @todos do %> 49 | <.cluster> 50 | 51 | 52 | 53 | 54 | <% end %> 55 |
56 | 62 | 63 | 64 | """ 65 | end 66 | end 67 | ``` 68 | 69 | ## Components 70 | 71 | | [Every Layout](https://every-layout.dev) Component | Phexel Component | 72 | | -------------------------------------------------- | ---------------- | 73 | | The Stack | `<.stack>` | 74 | | The Box | `<.box>` | 75 | | The Center | `<.center>` | 76 | | The Cluster | `<.cluster>` | 77 | | The Sidebar | `<.sidebar>` | 78 | | The Switcher | `<.switcher>` | 79 | | The Cover | `<.cover>` | 80 | | The Grid | `<.grid>` | 81 | | The Frame | `<.frame>` | 82 | | The Reel | `<.reel>` | 83 | | The Imposter | `<.imposter>` | 84 | | The Container | `<.container>` | 85 | | The Icon | `<.icon>` | 86 | 87 | Please refer to [Every Layout](https://every-layout.dev) if you want to learn more about when to use which component. 88 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUNNER_IMAGE="debian:bullseye-20210902-slim" 2 | 3 | # does user setup and gives 'install-packages' script, see https://github.com/gitpod-io/workspace-images 4 | FROM gitpod/workspace-base@sha256:5fcdb99366114646be6a82559aedb498b0f8b500192195782546664514ed166d as development 5 | 6 | USER gitpod 7 | 8 | ENV ASDF_VERSION=v0.10.0 9 | ENV ASDF_ERLANG_PLUGIN_COMMIT=8c5dacf4e73dcff1cae1471d21b86809e9bf51f5 10 | ENV ASDF_ELIXIR_PLUGIN_COMMIT=a4c42e10a7681afd4c87da144e9667865d5034c6 11 | 12 | # setup asdf-vm: https://asdf-vm.com/guide/getting-started.html 13 | RUN git clone --depth 1 https://github.com/asdf-vm/asdf.git $HOME/.asdf --branch ${ASDF_VERSION} \ 14 | && echo '. $HOME/.asdf/asdf.sh' >> $HOME/.bashrc \ 15 | && echo '. $HOME/.asdf/completions/asdf.bash' >> $HOME/.bashrc 16 | 17 | # in the following 'bash -i -c' is used to have asdf available as '-i' reads from .bashrc 18 | 19 | # Plugin Versions are pinned because we want a 💯 Percent reproducible dev environment. 20 | # install and pin erlang: https://github.com/asdf-vm/asdf-erlang 21 | RUN bash -i -c 'asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git && asdf plugin update erlang ${ASDF_ERLANG_PLUGIN_COMMIT}' 22 | 23 | # install and pin asdf elixir plugin: https://github.com/asdf-vm/asdf-elixir 24 | RUN bash -i -c 'asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git && asdf plugin update elixir ${ASDF_ELIXIR_PLUGIN_COMMIT}' 25 | 26 | # get .tool-versions from repo 27 | COPY .tool-versions $HOME/ 28 | 29 | # install versions specified in .tool-versions 30 | RUN bash -i -c 'asdf install' 31 | 32 | ENV ELIXIR_HEX_VERSION=v2.0.6 33 | 34 | # install # hex (v2.0.6 -> see: https://github.com/hexpm/hex/releases/tag/v2.0.6) 35 | RUN bash -i -c 'mix archive.install github hexpm/hex ref ${ELIXIR_HEX_VERSION} --force' 36 | 37 | # install # rebar (matching version depending on elixir? -> see: https://github.com/elixir-lang/elixir/blob/cc9e9b29a7b473010ed17f894e6a576983a9c294/lib/mix/lib/mix/tasks/local.rebar.ex#L124) 38 | RUN bash -i -c 'mix local.rebar --force' 39 | 40 | USER root 41 | 42 | # get inotify-tools for live reload from phoenix, see: https://hexdocs.pm/phoenix/installation.html#inotify-tools-for-linux-users 43 | RUN install-packages inotify-tools 44 | 45 | # install common utilities 46 | RUN install-packages \ 47 | curl 48 | 49 | USER gitpod 50 | 51 | # install flyctl, see: https://fly.io/docs/getting-started/installing-flyctl/ 52 | RUN curl -L https://fly.io/install.sh | sh \ 53 | && echo 'export FLYCTL_INSTALL="$HOME/.fly"' >> $HOME/.bashrc \ 54 | && echo 'export PATH="$HOME/.fly/bin:$PATH"' >> $HOME/.bashrc 55 | 56 | 57 | FROM development as builder 58 | 59 | USER root 60 | 61 | # prepare build dir 62 | WORKDIR /app 63 | 64 | # set build ENV 65 | ENV MIX_ENV="prod" 66 | 67 | # install mix dependencies 68 | COPY mix.exs mix.lock ./ 69 | RUN bash -i -c 'mix deps.get --only $MIX_ENV' 70 | RUN mkdir config 71 | 72 | # copy compile-time config files before we compile dependencies 73 | # to ensure any relevant config change will trigger the dependencies 74 | # to be re-compiled. 75 | COPY config/config.exs config/${MIX_ENV}.exs config/ 76 | RUN bash -i -c 'mix deps.compile' 77 | 78 | COPY priv priv 79 | 80 | # note: if your project uses a tool like https://purgecss.com/, 81 | # which customizes asset compilation based on what it finds in 82 | # your Elixir templates, you will need to move the asset compilation 83 | # step down so that `lib` is available. 84 | COPY assets assets 85 | 86 | # Compile the release 87 | COPY lib lib 88 | 89 | RUN bash -i -c 'mix compile' 90 | 91 | # compile assets 92 | RUN bash -i -c 'mix assets.deploy' 93 | 94 | # Changes to config/runtime.exs don't require recompiling the code 95 | COPY config/runtime.exs config/ 96 | 97 | COPY rel rel 98 | RUN bash -i -c 'mix release' 99 | 100 | # start a new build stage so that the final image will only contain 101 | # the compiled release and other runtime necessities 102 | FROM ${RUNNER_IMAGE} as release 103 | 104 | RUN apt-get update -y && apt-get install -y libstdc++6 openssl libncurses5 locales \ 105 | && apt-get clean && rm -f /var/lib/apt/lists/*_* 106 | 107 | # Set the locale 108 | RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen 109 | 110 | ENV LANG en_US.UTF-8 111 | ENV LANGUAGE en_US:en 112 | ENV LC_ALL en_US.UTF-8 113 | 114 | WORKDIR "/app" 115 | RUN chown nobody /app 116 | 117 | # Only copy the final release from the build stage 118 | COPY --from=builder --chown=nobody:root /app/_build/prod/rel/ephemeral ./ 119 | 120 | USER nobody 121 | 122 | CMD ["/app/bin/server"] 123 | 124 | # Appended by flyctl 125 | ENV ECTO_IPV6 true 126 | ENV ERL_AFLAGS "-proto_dist inet6_tcp" -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, 3 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 4 | "phoenix": {:hex, :phoenix, "1.7.12", "1cc589e0eab99f593a8aa38ec45f15d25297dd6187ee801c8de8947090b5a9d3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "d646192fbade9f485b01bc9920c139bfdd19d0f8df3d73fd8eaf2dfbe0d2837c"}, 5 | "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, 6 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.14", "70fa101aa0539e81bed4238777498f6215e9dda3461bdaa067cad6908110c364", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "82f6d006c5264f979ed5eb75593d808bbe39020f20df2e78426f4f2d570e2402"}, 7 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 8 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 9 | "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, 10 | "plug": {:hex, :plug, "1.16.0", "1d07d50cb9bb05097fdf187b31cf087c7297aafc3fed8299aac79c128a707e47", [: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", "cbf53aa1f5c4d758a7559c0bd6d59e286c2be0c6a1fac8cc3eee2f638243b93e"}, 11 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 12 | "styler": {:hex, :styler, "1.0.0-rc.0", "977c702b91b11e86ae1995f0f699a372a43e8df175f4878d7e9cc1678d0d7513", [:mix], [], "hexpm", "031624294295d47af7859ef43595092f33b861f0a88e44fae6366a54f1736a1a"}, 13 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 14 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 15 | "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, 16 | } 17 | -------------------------------------------------------------------------------- /assets/elc.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Ratios for maintaining visual consistency. */ 3 | --ratio: 2.71828; 4 | --s-5: calc(var(--s-4) / var(--ratio)); 5 | --s-4: calc(var(--s-3) / var(--ratio)); 6 | --s-3: calc(var(--s-2) / var(--ratio)); 7 | --s-2: calc(var(--s-1) / var(--ratio)); 8 | --s-1: calc(var(--s0) / var(--ratio)); 9 | --s0: 1rem; 10 | --s1: calc(var(--s0) * var(--ratio)); 11 | --s2: calc(var(--s1) * var(--ratio)); 12 | --s3: calc(var(--s2) * var(--ratio)); 13 | --s4: calc(var(--s3) * var(--ratio)); 14 | --s5: calc(var(--s4) * var(--ratio)); 15 | 16 | /* Zero value*/ 17 | --zero: 0; 18 | 19 | /* Measure width (characters per line) */ 20 | --measure: 60ch; 21 | } 22 | 23 | * { 24 | /* In general calculate from border-box. */ 25 | box-sizing: border-box; 26 | 27 | /* Clear default padding and margin. */ 28 | margin: 0; 29 | padding: 0; 30 | 31 | /* In general cap to nicely readable width. */ 32 | max-inline-size: var(--measure); 33 | } 34 | 35 | /* Exceptions made to above max-width rule. */ 36 | html, 37 | body, 38 | div, 39 | header, 40 | nav, 41 | main, 42 | footer { 43 | max-inline-size: none; 44 | } 45 | 46 | img { 47 | max-block-size: 100%; 48 | } 49 | 50 | /* BOX-LAYOUT */ 51 | .elc-box { 52 | --box-padding: var(--s0); 53 | --box-border-width: var(--s-5); 54 | padding: var(--box-padding); 55 | border: var(--box-border-width) solid; 56 | } 57 | 58 | .elc-box * { 59 | color: inherit; 60 | } 61 | 62 | /* CENTER-LAYOUT */ 63 | .elc-center { 64 | --center-padding-inline: var(--zero); 65 | box-sizing: content-box; 66 | margin-inline: auto; 67 | display: flex; 68 | flex-direction: column; 69 | align-items: center; 70 | padding-inline: var(--center-padding-inline); 71 | } 72 | 73 | /* CLUSTER-LAYOUT */ 74 | .elc-cluster { 75 | --cluster-gap: var(--s0); 76 | --cluster-justify-content: flex-start; 77 | --cluster-align-items: center; 78 | display: flex; 79 | flex-wrap: wrap; 80 | gap: var(--cluster-gap); 81 | justify-content: var(--cluster-justify-content); 82 | align-items: var(--cluster-align-items); 83 | } 84 | 85 | /* COVER-LAYOUT */ 86 | .elc-cover { 87 | --cover-padding: var(--s0); 88 | --cover-margin: var(--s0); 89 | --cover-min-block-size: 100%; 90 | display: flex; 91 | flex-direction: column; 92 | min-block-size: var(--cover-min-block-size); 93 | padding: var(--cover-padding); 94 | } 95 | 96 | .elc-cover>* { 97 | margin-block: var(--cover-margin); 98 | } 99 | 100 | /* If just on child exists, it is main element. */ 101 | .elc-cover> :only-child { 102 | margin-block: auto; 103 | } 104 | 105 | /* If two children exist, it is heading and main elements. */ 106 | .elc-cover> :last-child:nth-child(2) { 107 | margin-block: auto; 108 | } 109 | 110 | .elc-cover> :first-child:nth-last-child(2) { 111 | margin-block-start: 0; 112 | } 113 | 114 | /* If three children exist, it is heading, main and footer elements. */ 115 | .elc-cover> :nth-child(2):nth-last-child(2) { 116 | margin-block: auto; 117 | } 118 | 119 | .elc-cover> :first-child:nth-last-child(3) { 120 | margin-block-start: 0; 121 | } 122 | 123 | .elc-cover> :last-child::nth-child(3) { 124 | margin-block-end: 0; 125 | } 126 | 127 | /* FRAME-LAYOUT */ 128 | .elc-frame { 129 | --frame-width: 16; 130 | --frame-height: 9; 131 | aspect-ratio: var(--frame-width) / var(--frame-height); 132 | overflow: hidden; 133 | display: flex; 134 | justify-content: center; 135 | align-items: center; 136 | } 137 | 138 | .elc-frame>img, 139 | .elc-frame>video { 140 | inline-size: 100%; 141 | block-size: 100%; 142 | object-fit: cover; 143 | } 144 | 145 | /* GRID-LAYOUT */ 146 | .elc-grid { 147 | --grid-grid-gap: var(--s0); 148 | --grid-column-min-width: var(--s5); 149 | display: grid; 150 | grid-gap: var(--grid-grid-gap); 151 | } 152 | 153 | .elc-grid { 154 | grid-template-columns: repeat(auto-fit, 155 | minmax(min(var(--grid-column-min-width), 100%), 1fr)); 156 | } 157 | 158 | /* IMPOSTER-LAYOUT */ 159 | .elc-imposter { 160 | --imposter-position: absolute; 161 | --imposter-margin: var(--s0); 162 | position: var(--imposter-position); 163 | inset-block-start: 50%; 164 | inset-inline-start: 50%; 165 | transform: translate(-50%, -50%); 166 | max-inline-size: calc(100% - 2 * var(--imposter-margin)); 167 | max-block-size: calc(100% - 2 * var(--imposter-margin)); 168 | overflow: auto; 169 | } 170 | 171 | /* REEL-LAYOUT */ 172 | .elc-reel { 173 | --reel-block-size: auto; 174 | --reel-padding: var(--s0); 175 | --reel-gap: var(--s0); 176 | display: flex; 177 | overflow-x: auto; 178 | block-size: var(--reel-block-size); 179 | gap: var(--reel-gap); 180 | } 181 | 182 | .elc-reel>* { 183 | margin-block: var(--reel-padding); 184 | } 185 | 186 | .elc-reel> :first-child { 187 | margin-inline-start: var(--reel-padding); 188 | } 189 | 190 | .elc-reel>:last-child { 191 | margin-inline-end: var(--reel-padding); 192 | } 193 | 194 | .elc-reel>img { 195 | block-size: 100%; 196 | inline-size: auto; 197 | } 198 | 199 | /* SIDEBAR-LAYOUT */ 200 | .elc-sidebar { 201 | /* Flex basis for sidebar. */ 202 | --sidebar-flex-basis: initial; 203 | /* Minimum inline size of main content */ 204 | --sidebar-min-inline-size: 50%; 205 | --sidebar-gap: var(--s0); 206 | /* Can be set to 'row-reverse' to switch sidebar and main content. */ 207 | --sidebar-flex-direction: row; 208 | display: flex; 209 | flex-wrap: wrap; 210 | flex-direction: var(--sidebar-flex-direction); 211 | gap: var(--sidebar-gap); 212 | } 213 | 214 | .elc-sidebar> :first-child { 215 | flex-basis: var(--sidebar-flex-basis); 216 | flex-grow: 1; 217 | } 218 | 219 | .elc-sidebar> :last-child { 220 | flex-basis: 0; 221 | flex-grow: 999; 222 | min-inline-size: var(--sidebar-min-inline-size); 223 | } 224 | 225 | /* STACK-LAYOUT */ 226 | /* 227 | * Not directly nestable due to CSS variable override of outer stack. 228 | * Use one wrapping element between those with .elc-stack class. 229 | */ 230 | .elc-stack { 231 | --stack-margin: var(--s0); 232 | /* Should be set to 'auto' when splitting is desired. 233 | * Splitting occurs when the stack has exactly two elements. 234 | */ 235 | --stack-split: var(--zero); 236 | display: flex; 237 | flex-direction: column; 238 | justify-content: flex-start; 239 | } 240 | 241 | .elc-stack> :first-child:nth-last-child(2) { 242 | margin-block-end: var(--stack-split); 243 | } 244 | 245 | .elc-stack>*+* { 246 | margin-top: var(--stack-margin); 247 | } 248 | 249 | .elc-stack:only-child { 250 | block-size: 100%; 251 | } 252 | 253 | /* SWITCHER-LAYOUT */ 254 | .elc-switcher { 255 | --switcher-gap: var(--s0); 256 | --switcher-threshold: var(--measure); 257 | display: flex; 258 | flex-wrap: wrap; 259 | gap: var(--switcher-gap); 260 | 261 | } 262 | 263 | .elc-switcher>* { 264 | flex-grow: 1; 265 | flex-basis: calc((var(--switcher-threshold) - 100%) * 999); 266 | } 267 | 268 | /* ICON */ 269 | /* Gives icons the size of a box with edge length of lowercase letter 'x'. 270 | * As it is square, there is no need for logical properties. 271 | */ 272 | .elc-icon { 273 | width: 1ex; 274 | height: 1ex; 275 | } 276 | 277 | /* CONTAINER */ 278 | /* Allows to use the respective element in container queries. 279 | */ 280 | .elc-container { 281 | --container-name: containerNameNotSet; 282 | container-name: var(--container-name); 283 | container-type: inline-size; 284 | } --------------------------------------------------------------------------------