Compare commits

..

No commits in common. "f2f4c0531d706a1894bcc0e0f1acfd6ee7d61c1c" and "c935fbbc4270f8a3c3fc5faec2e24aebd1e95334" have entirely different histories.

35 changed files with 1374 additions and 1526 deletions

3
.gitignore vendored
View file

@ -46,6 +46,3 @@ npm-debug.log
# FreeBSD package # FreeBSD package
*.pkg *.pkg
# Local temp
_attic/

View file

@ -13,8 +13,7 @@ config :freedive,
config :freedive, config :freedive,
features: [ features: [
register_account: true, register_account: true
colorhash: true
] ]
# Configures the endpoint # Configures the endpoint

View file

@ -18,12 +18,6 @@ if config_env() == :prod do
config :freedive, :features, register_account: false config :freedive, :features, register_account: false
end end
# Colorhash feature is enabled by default.
# To disable, set the COLORHASH_ENABLE environment variable to "false".
if System.get_env("COLORHASH_ENABLE") == "false" do
config :freedive, :features, colorhash: false
end
database_path = database_path =
System.get_env("DATABASE_PATH") || System.get_env("DATABASE_PATH") ||
raise """ raise """

View file

@ -11,7 +11,6 @@ TLS_CERT_PATH="<%= @data_dir %>/tls.crt"
# Features # Features
REGISTER_ACCOUNT_ENABLE="false" REGISTER_ACCOUNT_ENABLE="false"
COLORHASH_ENABLE="true"
# Phoenix # Phoenix
PHX_SERVER=true PHX_SERVER=true

View file

@ -1,50 +0,0 @@
defmodule Freedive.Colorhash do
@moduledoc """
Given a string returns consistent HSL color.
"""
alias Freedive.Features
@seed "Free_ive"
@default_hsl {0, 0, 0}
def hsl(input, css: true) do
{hue, saturation, lightness} = hsl(input)
"hsl(#{hue}, #{saturation}%, #{lightness}%)"
end
def hsl(input) do
if Features.enabled?(:colorhash) do
input = String.downcase(input) <> @seed
hash = :erlang.phash2(input, 2_147_483_647)
hue = calculate_hue(hash)
saturation = 80 + rem(hash, 21)
lightness = 30 + rem(hash, 31)
{hue, saturation, lightness}
else
@default_hsl
end
end
@doc """
Calculates hue avoiding low contrast colors.
"""
def calculate_hue(hash) do
base_hue = rem(hash, 360)
# Ranges to exclude (yellow to light green, cyan)
excluded_ranges = [
# yellow
{40, 80},
# cyan
{175, 185},
]
# Adjust the hue to skip over excluded ranges
Enum.reduce(excluded_ranges, base_hue, fn {start, stop}, acc_hue ->
if acc_hue >= start and acc_hue <= stop, do: stop + (acc_hue - start + 1), else: acc_hue
end)
end
end

View file

@ -53,7 +53,7 @@ defmodule FreediveWeb do
quote do quote do
use Phoenix.LiveView, use Phoenix.LiveView,
layout: {FreediveWeb.Layouts, :app}, layout: {FreediveWeb.Layouts, :app},
global_prefixes: ["is-", "has-"] global_prefixes: ["is-", "has-", "flex-", "justify-", "align-"]
unquote(html_helpers()) unquote(html_helpers())
end end
@ -69,7 +69,7 @@ defmodule FreediveWeb do
def html do def html do
quote do quote do
use Phoenix.Component, global_prefixes: ["is-", "has-"] use Phoenix.Component, global_prefixes: ["is-", "has-", "flex-", "justify-", "align-"]
# Import convenience functions from controllers # Import convenience functions from controllers
import Phoenix.Controller, import Phoenix.Controller,
@ -86,9 +86,8 @@ defmodule FreediveWeb do
import Phoenix.HTML import Phoenix.HTML
# Core UI components and translation # Core UI components and translation
import FreediveWeb.CoreComponents, only: [header: 1] import FreediveWeb.CoreComponents, only: [header: 1]
import Liliform.Components
import FreediveWeb.Gettext import FreediveWeb.Gettext
# Import custom components
use Liliform
# Shortcut for generating JS commands # Shortcut for generating JS commands
alias Phoenix.LiveView.JS alias Phoenix.LiveView.JS

View file

@ -12,73 +12,45 @@
</.navbar_brand> </.navbar_brand>
<.navbar_menu id="navbar_top" class="mr-1"> <.navbar_menu id="navbar_top" class="mr-1">
<%= if @current_user do %> <%!-- <%= if @current_user do %>
<.navbar_start> <.navbar_start>
<.navbar_item has-dropdown is-hoverable> <.navbar_item has-dropdown is-hoverable>
<.navbar_link> <.navbar_link>
<.icon for="compute" aria-hidden="true"> Compute
Compute
</.icon>
</.navbar_link> </.navbar_link>
<.navbar_dropdown> <.navbar_dropdown>
<.link href={~p"/compute/apps"} class="navbar-item"> <.navbar_item>
Apps <.link>Bastille Jails</.link>
</.link> </.navbar_item>
<.link href={~p"/compute/tasks"} class="navbar-item">
Tasks
</.link>
</.navbar_dropdown> </.navbar_dropdown>
</.navbar_item> </.navbar_item>
<.navbar_item has-dropdown is-hoverable> <.navbar_item has-dropdown is-hoverable>
<.navbar_link> <.navbar_link>
<.icon for="storage" aria-hidden="true"> Storage
Storage
</.icon>
</.navbar_link> </.navbar_link>
<.navbar_dropdown> <.navbar_dropdown>
<.link href={~p"/storage/local"} class="navbar-item"> <.navbar_item>
Local <.link>Shared Folders</.link>
</.link> </.navbar_item>
<.link href={~p"/storage/shared"} class="navbar-item">
Shared
</.link>
<.link href={~p"/storage/remote"} class="navbar-item">
Remote
</.link>
</.navbar_dropdown> </.navbar_dropdown>
</.navbar_item> </.navbar_item>
<.navbar_item has-dropdown is-hoverable> <.navbar_item has-dropdown is-hoverable>
<.navbar_link> <.navbar_link>
<.icon for="network" aria-hidden="true"> Network
Network
</.icon>
</.navbar_link> </.navbar_link>
<.navbar_dropdown> <.navbar_dropdown>
<.link href={~p"/network/public"} class="navbar-item"> <.navbar_item>
Public <.link>Private Network</.link>
</.link> </.navbar_item>
<.link href={~p"/network/private"} class="navbar-item">
Private
</.link>
<.navbar_divider />
<.link href={~p"/network/endpoints"} class="navbar-item">
Endpoints
</.link>
</.navbar_dropdown> </.navbar_dropdown>
</.navbar_item> </.navbar_item>
</.navbar_start> </.navbar_start>
<% end %> <% end %> --%>
<.navbar_end> <.navbar_end>
<%= if @current_user do %> <%= if @current_user do %>
@ -88,20 +60,30 @@
<.navbar_item has-dropdown is-hoverable> <.navbar_item has-dropdown is-hoverable>
<.navbar_link> <.navbar_link>
<.icon for="system" aria-hidden="true"> <span class="icon mr-2">
System <Lucideicons.bot aria-hidden="true" />
</.icon> </span>
System
</.navbar_link> </.navbar_link>
<.navbar_dropdown> <.navbar_dropdown>
<.link href={~p"/updates"} class="navbar-item"> <.link href={~p"/updates"} class="navbar-item">
<span class="icon mr-2">
<Lucideicons.hard_drive_download aria-hidden="true" />
</span>
Software updates Software updates
</.link> </.link>
<.navbar_divider /> <.navbar_divider />
<.link href={~p"/packages"} class="navbar-item"> <.link href={~p"/packages"} class="navbar-item">
<span class="icon mr-2">
<Lucideicons.package aria-hidden="true" />
</span>
Packages Packages
</.link> </.link>
<.link href={~p"/services"} class="navbar-item"> <.link href={~p"/services"} class="navbar-item">
<span class="icon mr-2">
<Lucideicons.puzzle aria-hidden="true" />
</span>
Services Services
</.link> </.link>
</.navbar_dropdown> </.navbar_dropdown>
@ -109,22 +91,29 @@
<% end %> <% end %>
<.navbar_item has-dropdown is-hoverable> <.navbar_item has-dropdown is-hoverable>
<.navbar_link> <.navbar_link>
<.icon for="account" aria-hidden="true"> <span class="icon mr-2">
Account <Lucideicons.circle_user aria-hidden="true" />
</.icon> </span>
Account
</.navbar_link> </.navbar_link>
<.navbar_dropdown> <.navbar_dropdown>
<%= if @current_user do %> <%= if @current_user do %>
<.link href={~p"/users/settings"} class="navbar-item"> <.link href={~p"/users/settings"} class="navbar-item">
<span class="icon mr-2">
<Lucideicons.user_cog aria-hidden="true" />
</span>
Settings Settings
</.link> </.link>
<.link href={~p"/users/log_out"} method="delete" class="navbar-item"> <.link href={~p"/users/log_out"} method="delete" class="navbar-item">
<span class="icon mr-2">
<Lucideicons.log_out aria-hidden="true" />
</span>
Log out Log out
</.link> </.link>
<% else %> <% else %>
<.link href={~p"/users/log_in"} class="navbar-item"> <.link href={~p"/users/log_in"} class="navbar-item">
Log in <Lucideicons.log_in aria-hidden="true" /> Log in
</.link> </.link>
<% end %> <% end %>
</.navbar_dropdown> </.navbar_dropdown>

File diff suppressed because it is too large Load diff

View file

@ -14,21 +14,11 @@ defmodule FreediveWeb.HomeLive do
<a>System</a> <a>System</a>
</.panel_tabs> </.panel_tabs>
<.panel_tabs is-hidden-tablet> <.panel_tabs is-hidden-tablet>
<a class="is-active" title="All"> <a class="is-active"><Lucideicons.infinity aria-hidden="true" /></a>
<.icon for="infinity" color="auto" /> <a><Lucideicons.binary aria-hidden="true" /></a>
</a> <a><Lucideicons.hard_drive aria-hidden="true" /></a>
<a title="Compute"> <a><Lucideicons.earth aria-hidden="true" /></a>
<.icon for="binary" color="auto" /> <a><Lucideicons.bot aria-hidden="true" /></a>
</a>
<a title="Storage">
<.icon for="hard-drive" color="auto" />
</a>
<a title="Network">
<.icon for="earth" color="auto" />
</a>
<a title="System">
<.icon for="bot" color="auto" />
</a>
</.panel_tabs> </.panel_tabs>
<.panel_block> <.panel_block>
@ -45,23 +35,29 @@ defmodule FreediveWeb.HomeLive do
</span> </span>
</.control> </.control>
</.panel_block> </.panel_block>
<.link patch={~p"/users/settings"} class="panel-block pt-1">
<span class="panel-icon">
<Lucideicons.user_cog aria-hidden="true" />
</span>
<span class="mt-2 ml-2">Account settings</span>
</.link>
<.link patch={~p"/services"} class="panel-block pt-1"> <.link patch={~p"/services"} class="panel-block pt-1">
<span class="panel-icon"> <span class="panel-icon">
<.icon for="puzzle" color="auto" aria-hidden="true" /> <Lucideicons.puzzle aria-hidden="true" />
</span> </span>
<div class="mt-2 ml-2">Services</div> <div class="mt-2 ml-2">System services</div>
</.link> </.link>
<.link patch={~p"/packages"} class="panel-block pt-1"> <.link patch={~p"/packages"} class="panel-block pt-1">
<span class="panel-icon"> <span class="panel-icon">
<.icon for="package" color="auto" aria-hidden="true" /> <Lucideicons.package aria-hidden="true" />
</span> </span>
<span class="mt-2 ml-2">Packages</span> <span class="mt-2 ml-2">System packages</span>
</.link> </.link>
<.link patch={~p"/updates"} class="panel-block pt-1"> <.link patch={~p"/updates"} class="panel-block pt-1">
<span class="panel-icon"> <span class="panel-icon">
<.icon for="hard-drive-download" color="auto" aria-hidden="true" /> <Lucideicons.hard_drive_download aria-hidden="true" />
</span> </span>
<span class="mt-2 ml-2">Software updates</span> <span class="mt-2 ml-2">System software updates</span>
</.link> </.link>
</.panel> </.panel>
</.section> </.section>

View file

@ -1,79 +0,0 @@
defmodule FreediveWeb.LiliformLive do
use FreediveWeb, :live_view
def render(assigns) do
~H"""
<.block class="px-2 py-4">
<.panel is-info>
<.panel_heading>
Home
</.panel_heading>
<.panel_tabs is-hidden-mobile>
<a class="is-active">All</a>
<a>Compute</a>
<a>Storage</a>
<a>Network</a>
<a>System</a>
</.panel_tabs>
<.panel_tabs is-hidden-tablet>
<a title="All" is-active>
<.icon for="all" color="auto" />
</a>
<a title="Compute">
<.icon for="compute" color="auto" />
</a>
<a title="Storage">
<.icon for="storage" color="auto" />
</a>
<a title="Network">
<.icon for="network" color="auto" />
</a>
<a title="System">
<.icon for="system" color="auto" />
</a>
</.panel_tabs>
<.panel_block>
<.control has-icons-left>
<input
class="input is-info"
type="text"
placeholder="Search"
name="search"
value={@query}
/>
<.icon for="search" size="1.5rem" aria-hidden="true" is-left />
</.control>
</.panel_block>
</.panel>
</.block>
<.section>
<.box>
<.button phx-click="color" phx-value-enable="true">
Color
</.button>
<.button phx-click="color" phx-value-enable="false">
Grayscale
</.button>
</.box>
</.section>
"""
end
def mount(_params, _session, socket) do
socket = assign(socket, query: "all")
{:ok, socket}
end
def handle_event("color", %{"enable" => "true"}, socket) do
Freedive.Features.enable(:colorhash)
{:noreply, assign(socket, query: "color")}
end
def handle_event("color", %{"enable" => "false"}, socket) do
Freedive.Features.disable(:colorhash)
{:noreply, assign(socket, query: "grayscale")}
end
end

View file

@ -69,7 +69,6 @@ defmodule FreediveWeb.Router do
live_session :require_authenticated_user, live_session :require_authenticated_user,
on_mount: [{FreediveWeb.UserAuth, :ensure_authenticated}] do on_mount: [{FreediveWeb.UserAuth, :ensure_authenticated}] do
live "/", HomeLive live "/", HomeLive
live "/liliform", LiliformLive
live "/users/settings", UserSettingsLive, :edit live "/users/settings", UserSettingsLive, :edit
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
end end

View file

@ -1,24 +0,0 @@
defmodule Liliform do
defmacro __using__(_) do
quote do
import Liliform.Block
import Liliform.Box
import Liliform.Button
import Liliform.Column
import Liliform.Container
import Liliform.Content
import Liliform.Control
import Liliform.Flash
import Liliform.Hero
import Liliform.Icon
import Liliform.Input
import Liliform.Label
import Liliform.Media
import Liliform.Navbar
import Liliform.Panel
import Liliform.Section
import Liliform.SimpleForm
import Liliform.Title
end
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Block do
use Liliform.Component
@doc """
Renders a block.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def block(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["block", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Box do
use Liliform.Component
@doc """
Renders a box.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def box(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["box", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end

View file

@ -1,35 +0,0 @@
defmodule Liliform.BulmaHelper do
def set_bulma_classes(assigns) do
rest_classes =
assigns.rest
|> Map.keys()
|> Enum.map(&to_string/1)
is_bulma_classes =
rest_classes
|> Enum.filter(&String.starts_with?(&1, "is-"))
has_bulma_classes =
rest_classes
|> Enum.filter(&String.starts_with?(&1, "has-"))
bulma_classes =
is_bulma_classes ++ has_bulma_classes
rest =
assigns.rest
|> Map.reject(fn {k, _} -> String.starts_with?(to_string(k), "is-") end)
|> Map.reject(fn {k, _} -> String.starts_with?(to_string(k), "has-") end)
assigns = Map.put(assigns, :rest, rest)
assigns =
Map.put(
assigns,
:class,
Map.get(assigns, :class, "") <> Enum.join(bulma_classes, " ")
)
assigns
end
end

View file

@ -1,24 +0,0 @@
defmodule Liliform.Button do
use Liliform.Component
@doc """
Renders button.
"""
attr :type, :string, default: "button", doc: "button type"
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def button(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<button class={["button", @class]} type={@type} {@rest}>
<%= render_slot(@inner_block) %>
</button>
"""
end
end

View file

@ -1,58 +0,0 @@
defmodule Liliform.Colorhash do
@moduledoc """
Given a string returns HSL color.
"""
alias Freedive.Features
require Logger
@seed "zeni"
@default_color {0, 0, 0}
@doc """
Given a string returns HSL color as a CSS string.
"""
def hsl(input) do
{hue, saturation, lightness} = hsl(input, raw: true)
"hsl(#{hue}, #{saturation}%, #{lightness}%)"
end
@doc """
Given a string returns HSL color.
"""
def hsl(input, raw: true) do
if Features.enabled?(:colorhash) do
Logger.debug("Colorhash enabled")
input = String.downcase(input) <> @seed
hash = :erlang.phash2(input, 2_147_483_647)
hue = calculate_hue(hash)
saturation = 80 + rem(hash, 21)
lightness = 30 + rem(hash, 31)
{hue, saturation, lightness}
else
Logger.debug("Colorhash disabled")
@default_color
end
end
@doc """
Calculates hue avoiding low contrast colors.
"""
def calculate_hue(hash) do
base_hue = rem(hash, 360)
# Ranges to exclude (yellow to light green, cyan)
excluded_ranges = [
# yellow
{40, 80},
# cyan
{175, 185}
]
# Adjust the hue to skip over excluded ranges
Enum.reduce(excluded_ranges, base_hue, fn {start, stop}, acc_hue ->
if acc_hue >= start and acc_hue <= stop, do: stop + (acc_hue - start + 1), else: acc_hue
end)
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Column do
use Liliform.Component
@doc """
Renders a column.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def column(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["column", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end

View file

@ -1,17 +0,0 @@
defmodule Liliform.Component do
defmacro __using__(_) do
quote do
# Bulma prefixes
use Phoenix.Component,
global_prefixes: [
"is-",
"has-"
]
alias Phoenix.LiveView.JS
import Liliform.Translation
import Liliform.BulmaHelper
end
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Container do
use Liliform.Component
@doc """
Renders a container.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def container(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["container", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Content do
use Liliform.Component
@doc """
Renders a content.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def content(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["content", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Control do
use Liliform.Component
@doc """
Renders a control.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def control(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["control", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end

View file

@ -1,143 +0,0 @@
defmodule Liliform.Flash do
use Liliform.Component
alias Phoenix.LiveView.JS
import FreediveWeb.Gettext
import Liliform.Button
import Liliform.Content
import Liliform.Hero
import Liliform.Media
import Liliform.Title
@doc """
Renders flash notices.
## Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
"""
attr :id, :string, doc: "the optional id of flash container"
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
attr :title, :string, default: nil, doc: "optional title for flash message"
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
slot :inner_block, doc: "the optional inner block that renders the flash message"
def flash(assigns) do
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
# IO.inspect(assigns.rest)
~H"""
<.hero
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class={
Enum.join(
[
"is-small",
@kind == :info && "is-info",
@kind == :error && "is-warning"
],
" "
)
}
{@rest}
>
<.media>
<.media_left>
<Lucideicons.info :if={@kind == :info} class="icon-small" aria-hidden />
<Lucideicons.triangle_alert :if={@kind == :error} class="icon-small" aria-hidden />
</.media_left>
<.media_content>
<.content>
<.title :if={@title} is-4>
<%= @title %>
</.title>
<.subtitle is-6>
<%= msg %>
</.subtitle>
</.content>
</.media_content>
<.media_right is-hidden-touch>
<.button aria-label={gettext("close")} phx-click={hide("##{@id}")}>
<Lucideicons.circle_x class="h-10 w-10" aria-hidden />
</.button>
</.media_right>
</.media>
</.hero>
"""
end
@doc """
Shows the flash group with standard titles and content.
## Examples
<.flash_group flash={@flash} />
"""
attr :flash, :map, required: true, doc: "the map of flash messages"
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
def flash_group(assigns) do
~H"""
<div id={@id}>
<.flash kind={:info} title={gettext("Info")} flash={@flash} is-info />
<.flash kind={:error} title={gettext("Error")} flash={@flash} is-danger />
<.flash
id="client-error"
kind={:error}
title={gettext("We can't find the internet")}
phx-disconnected={show("#client-error")}
phx-connected={hide("#client-error")}
is-hidden
is-warning
>
<%= gettext("Attempting to reconnect") %>
<Lucideicons.refresh_cw class="h-4 w-4 animate-spin is-inline-block" aria-hidden />
</.flash>
<.flash
id="server-error"
kind={:error}
title={gettext("Something went wrong!")}
phx-disconnected={show("#server-error")}
phx-connected={hide("#server-error")}
is-hidden
is-warning
>
<%= gettext("Hang in there while we get back on track") %>
<Lucideicons.refresh_cw class="h-4 w-4 animate-spin is-inline-block" aria-hidden />
</.flash>
</div>
"""
end
## JS Commands
def show(js \\ %JS{}, selector) do
JS.remove_class(js, "is-hidden",
to: selector,
time: 100,
transition:
{"transition-all transform ease-out duration-300",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
"opacity-100 translate-y-0 sm:scale-100"}
)
end
def hide(js \\ %JS{}, selector) do
JS.add_class(js, "is-hidden",
to: selector,
time: 200,
transition:
{"transition-all transform ease-in duration-200",
"opacity-100 translate-y-0 sm:scale-100",
"opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
)
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Hero do
use Liliform.Component
@doc """
Renders a hero section.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def hero(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<section class={["hero", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</section>
"""
end
end

View file

@ -1,141 +0,0 @@
defmodule Liliform.Icon do
@moduledoc """
Bulma icon element.
The icon element is a container for any type of icon font.
Because the icons can take a few seconds to load,
and because you want control over the space
the icons will take, you can use the icon class
as a reliable square container that will prevent
the page to "jump" on page load.
By default, the icon container will take up
exactly 1.5rem x 1.5rem.
The icon itself is sized accordingly to the icon library you're using.
For example, Font Awesome 5 icons will inherit the font size.
"""
use Liliform.Component
alias Liliform.Colorhash
alias Freedive.Features
@doc """
Renders an icon.
## For
The icon name is set using the `for` attribute.
## Color
Set color to `auto` to automatically choose/generate color
based on the icon name.
You can also set the color to any valid CSS color value.
## Size
The icon size can be set using the `size` attribute.
By default, the size is set to `1.5rem`.
Example:
<.icon for="home" />
<.icon for="info" color="auto" />
<.icon for="alert" color="#f44" />
<.icon for="settings" color="blue" />
<.icon for="error" color="hsl(0, 100%, 50%)" />
<.icon for="info" size="2rem" />
"""
attr :for, :string, required: true, doc: "icon name"
attr :size, :string, default: nil, doc: "size of icon"
attr :color, :string, default: nil, doc: "color of icon"
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: false
def icon(assigns) do
assigns =
assigns
|> set_bulma_classes()
assigns =
if assigns.size != nil do
Map.put(assigns, :style, "width: #{assigns.size}; height: #{assigns.size};")
else
assigns
end
~H"""
<span class="icon-text">
<span class={["icon", @class]} {@rest}>
<.icon_svg for={@for} height={@size} width={@size} {icon_color(assigns)} />
</span>
<%= if @inner_block != [] do %>
<span>
<%= render_slot(@inner_block) %>
</span>
<% end %>
</span>
"""
end
defp icon_svg(assigns) do
base_assigns =
assigns
|> Map.reject(fn {k, _} -> k in [:for, :size, :rest] end)
|> Map.reject(fn {k, v} -> k in [:color] and v == "" end)
apply(Lucideicons, icon_name(assigns), [base_assigns])
end
defp icon_color(assigns) do
if Features.enabled?(:colorhash) do
color =
case assigns.color do
nil -> ""
"auto" -> Colorhash.hsl(assigns.for)
color -> color
end
case assigns.for do
"alert" -> [class: "has-text-danger"]
"info" -> [class: "has-text-info"]
"success" -> [class: "has-text-success"]
"warning" -> [class: "has-text-warning"]
"error" -> [class: "has-text-danger"]
"all" -> [color: "magenta"]
"compute" -> [color: "blue"]
"storage" -> [color: "green"]
"network" -> [color: "orange"]
"system" -> [color: "purple"]
"account" -> [color: "darkblue"]
_ -> [color: color]
end
else
[class: "has-text-dark"]
end
end
defp icon_name(assigns) do
name =
assigns.for
|> String.trim()
|> String.downcase()
|> String.replace("-", "_")
case name do
"alert" -> :triangle_alert
"compute" -> :binary
"storage" -> :hard_drive
"network" -> :earth
"system" -> :bot
"account" -> :user
"all" -> :infinity
lucide_name -> String.to_atom(lucide_name)
end
end
end

View file

@ -1,157 +0,0 @@
defmodule Liliform.Input do
use Liliform.Component
import Liliform.Label
@doc """
Renders an input with label and error messages.
A `Phoenix.HTML.FormField` may be passed as argument,
which is used to retrieve the input name, id, and values.
Otherwise all attributes may be passed explicitly.
## Types
This function accepts all HTML input types, considering that:
* You may also set `type="select"` to render a `<select>` tag
* `type="checkbox"` is used exclusively to render boolean values
* For live file uploads, see `Phoenix.Component.live_file_input/1`
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
for more information.
## Examples
<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />
"""
attr :id, :any, default: nil
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
range radio search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: []
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
attr :rest, :global,
include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
multiple pattern placeholder readonly required rows size step)
slot :inner_block
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
end)
~H"""
<div class="field" phx-feedback-for={@name}>
<label class="label">
<input type="hidden" name={@name} value="false" />
<input
type="checkbox"
id={@id}
name={@name}
value="true"
checked={@checked}
class="checkbox"
{@rest}
/>
<%= @label %>
</label>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div class="field" phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<select id={@id} name={@name} class="select" multiple={@multiple} {@rest}>
<option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
</select>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div class="field" phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<textarea
id={@id}
name={@name}
class={[
"textarea",
@errors == [] && "",
@errors != [] && "is-danger"
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
~H"""
<div class="field" phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"input",
@errors == [] && "",
@errors != [] && "is-danger"
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
@doc """
Generates a generic error message.
"""
slot :inner_block, required: true
def error(assigns) do
~H"""
<p class="mt-3 phx-no-feedback:hidden">
<Lucideicons.circle_alert aria-hidden />
<%= render_slot(@inner_block) %>
</p>
"""
end
end

View file

@ -1,19 +0,0 @@
defmodule Liliform.Label do
use Liliform.Component
@doc """
Renders a label.
"""
attr :for, :string, default: nil, doc: "the for attribute of the label"
attr :class, :string, default: nil, doc: "the optional class of the label"
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the label"
slot :inner_block, required: true, doc: "the inner block that renders the label content"
def label(assigns) do
~H"""
<label for={@for} class={@class} {@rest}>
<%= render_slot(@inner_block) %>
</label>
"""
end
end

View file

@ -1,80 +0,0 @@
defmodule Liliform.Media do
use Liliform.Component
@doc """
Renders a media object.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def media(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<article class={["media", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</article>
"""
end
@doc """
Renders a media-left.
"""
attr :rest, :global
slot :inner_block, required: true
def media_left(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<figure class="media-left" {@rest}>
<%= render_slot(@inner_block) %>
</figure>
"""
end
@doc """
Renders a media-content.
"""
attr :rest, :global
slot :inner_block, required: true
def media_content(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class="media-content" {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a media-right.
"""
attr :rest, :global
slot :inner_block, required: true
def media_right(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class="media-right" {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
end

View file

@ -1,209 +0,0 @@
defmodule Liliform.Navbar do
use Liliform.Component
@doc """
Renders a navbar.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<nav class={["navbar", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</nav>
"""
end
@doc """
Renders a navbar-brand.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar_brand(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["navbar-brand", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a navbar-menu.
"""
attr :id, :string, required: true
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar_menu(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div id={@id} class={["navbar-menu", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a navbar-start.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar_start(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["navbar-start", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a navbar-end.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar_end(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["navbar-end", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a navbar-item.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar_item(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["navbar-item", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a navbar-dropdown.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar_dropdown(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["navbar-dropdown", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a navbar-link.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def navbar_link(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<a class={["navbar-link", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</a>
"""
end
@doc """
Renders a navbar-divider.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
def navbar_divider(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<hr class={["navbar-divider", @class]} {@rest} />
"""
end
@doc """
Renders a navbar-burger.
"""
attr :target, :string, required: true
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
def navbar_burger(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<a
role="button"
class={["navbar-burger", @class]}
aria-label="menu"
aria-expanded="false"
data-target={@target}
{@rest}
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
"""
end
end

View file

@ -1,83 +0,0 @@
defmodule Liliform.Panel do
use Liliform.Component
@doc """
Renders a panel.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def panel(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<nav class={["panel", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</nav>
"""
end
@doc """
Renders a panel-heading.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def panel_heading(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<p class={["panel-heading", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</p>
"""
end
@doc """
Renders a panel-block.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def panel_block(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<div class={["panel-block", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</div>
"""
end
@doc """
Renders a panel-tabs.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def panel_tabs(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<p class={["panel-tabs", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</p>
"""
end
end

View file

@ -1,23 +0,0 @@
defmodule Liliform.Section do
use Liliform.Component
@doc """
Renders a section.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def section(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<section class={["section", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</section>
"""
end
end

View file

@ -1,39 +0,0 @@
defmodule Liliform.SimpleForm do
use Liliform.Component
@doc """
Renders a simple form.
## Examples
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label="Email"/>
<.input field={@form[:username]} label="Username" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
attr :for, :any, required: true, doc: "the datastructure for the form"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :rest, :global,
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
doc: "the arbitrary HTML attributes to apply to the form tag"
slot :inner_block, required: true
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest}>
<div class="box">
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-4">
<%= render_slot(action, f) %>
</div>
</div>
</.form>
"""
end
end

View file

@ -1,43 +0,0 @@
defmodule Liliform.Title do
use Liliform.Component
@doc """
Renders a title.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def title(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<h1 class={["title", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</h1>
"""
end
@doc """
Renders a subtitle.
"""
attr :class, :string, default: "", doc: "additional classes"
attr :rest, :global
slot :inner_block, required: true
def subtitle(assigns) do
assigns =
assigns
|> set_bulma_classes()
~H"""
<h2 class={["subtitle", @class]} {@rest}>
<%= render_slot(@inner_block) %>
</h2>
"""
end
end

View file

@ -1,29 +0,0 @@
defmodule Liliform.Translation do
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# However the error messages in our forms and APIs are generated
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do
Gettext.dngettext(FreediveWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(FreediveWeb.Gettext, "errors", msg, opts)
end
end
@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
end

View file

@ -1,25 +0,0 @@
defmodule Freedive.ColorhashTest do
use ExUnit.Case
alias Liliform.Colorhash
alias Freedive.Features
test "hsl" do
assert Colorhash.hsl("foo", raw: true) == {275, 85, 34}
assert Colorhash.hsl("bar", raw: true) == {309, 92, 46}
assert Colorhash.hsl("baz", raw: true) == {307, 87, 37}
end
test "hsl with css" do
assert Colorhash.hsl("foo") == "hsl(275, 85%, 34%)"
assert Colorhash.hsl("bar") == "hsl(309, 92%, 46%)"
assert Colorhash.hsl("baz") == "hsl(307, 87%, 37%)"
end
test "hsl with disabled feature" do
Features.disable(:colorhash)
assert Colorhash.hsl("foo", raw: true) == {0, 0, 0}
assert Colorhash.hsl("bar", raw: true) == {0, 0, 0}
assert Colorhash.hsl("baz", raw: true) == {0, 0, 0}
Features.enable(:colorhash)
end
end