forked from hiway/freedive
Redo Liliform for clarity
This commit is contained in:
parent
beea7b0caf
commit
707111fdac
30 changed files with 1348 additions and 1403 deletions
|
@ -4,6 +4,8 @@ defmodule Freedive.Colorhash do
|
||||||
"""
|
"""
|
||||||
alias Freedive.Features
|
alias Freedive.Features
|
||||||
|
|
||||||
|
@seed "Free_ive"
|
||||||
|
|
||||||
@default_hsl {0, 0, 0}
|
@default_hsl {0, 0, 0}
|
||||||
|
|
||||||
def hsl(input, css: true) do
|
def hsl(input, css: true) do
|
||||||
|
@ -13,7 +15,7 @@ defmodule Freedive.Colorhash do
|
||||||
|
|
||||||
def hsl(input) do
|
def hsl(input) do
|
||||||
if Features.enabled?(:colorhash) do
|
if Features.enabled?(:colorhash) do
|
||||||
input = String.downcase(input)
|
input = String.downcase(input) <> @seed
|
||||||
hash = :erlang.phash2(input, 2_147_483_647)
|
hash = :erlang.phash2(input, 2_147_483_647)
|
||||||
|
|
||||||
hue = calculate_hue(hash)
|
hue = calculate_hue(hash)
|
||||||
|
@ -35,7 +37,7 @@ defmodule Freedive.Colorhash do
|
||||||
# Ranges to exclude (yellow to light green, cyan)
|
# Ranges to exclude (yellow to light green, cyan)
|
||||||
excluded_ranges = [
|
excluded_ranges = [
|
||||||
# yellow
|
# yellow
|
||||||
{50, 70},
|
{40, 80},
|
||||||
# cyan
|
# cyan
|
||||||
{175, 185},
|
{175, 185},
|
||||||
]
|
]
|
||||||
|
|
|
@ -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-", "flex-", "justify-", "align-"]
|
global_prefixes: ["is-", "has-"]
|
||||||
|
|
||||||
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-", "flex-", "justify-", "align-"]
|
use Phoenix.Component, global_prefixes: ["is-", "has-"]
|
||||||
|
|
||||||
# Import convenience functions from controllers
|
# Import convenience functions from controllers
|
||||||
import Phoenix.Controller,
|
import Phoenix.Controller,
|
||||||
|
@ -86,8 +86,9 @@ 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
|
||||||
|
|
|
@ -12,45 +12,73 @@
|
||||||
</.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>
|
||||||
<.navbar_item>
|
<.link href={~p"/compute/apps"} class="navbar-item">
|
||||||
<.link>Bastille Jails</.link>
|
Apps
|
||||||
</.navbar_item>
|
</.link>
|
||||||
|
|
||||||
|
<.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>
|
||||||
<.navbar_item>
|
<.link href={~p"/storage/local"} class="navbar-item">
|
||||||
<.link>Shared Folders</.link>
|
Local
|
||||||
</.navbar_item>
|
</.link>
|
||||||
|
|
||||||
|
<.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>
|
||||||
<.navbar_item>
|
<.link href={~p"/network/public"} class="navbar-item">
|
||||||
<.link>Private Network</.link>
|
Public
|
||||||
</.navbar_item>
|
</.link>
|
||||||
|
|
||||||
|
<.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 %>
|
||||||
|
@ -60,30 +88,20 @@
|
||||||
|
|
||||||
<.navbar_item has-dropdown is-hoverable>
|
<.navbar_item has-dropdown is-hoverable>
|
||||||
<.navbar_link>
|
<.navbar_link>
|
||||||
<span class="icon mr-2">
|
<.icon for="system" aria-hidden="true">
|
||||||
<Lucideicons.bot aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
System
|
System
|
||||||
|
</.icon>
|
||||||
</.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>
|
||||||
|
@ -91,29 +109,22 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<.navbar_item has-dropdown is-hoverable>
|
<.navbar_item has-dropdown is-hoverable>
|
||||||
<.navbar_link>
|
<.navbar_link>
|
||||||
<span class="icon mr-2">
|
<.icon for="account" aria-hidden="true">
|
||||||
<Lucideicons.circle_user aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
Account
|
Account
|
||||||
|
</.icon>
|
||||||
</.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">
|
||||||
<Lucideicons.log_in aria-hidden="true" /> Log in
|
Log in
|
||||||
</.link>
|
</.link>
|
||||||
<% end %>
|
<% end %>
|
||||||
</.navbar_dropdown>
|
</.navbar_dropdown>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,5 @@
|
||||||
defmodule FreediveWeb.HomeLive do
|
defmodule FreediveWeb.HomeLive do
|
||||||
use FreediveWeb, :live_view
|
use FreediveWeb, :live_view
|
||||||
import Freedive.Colorhash
|
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
@ -15,19 +14,21 @@ 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">
|
<a class="is-active" title="All">
|
||||||
<Lucideicons.infinity style={"color: #{hsl("all", css: true)}"} aria-hidden="true" />
|
<.icon for="infinity" color="auto" />
|
||||||
</a>
|
</a>
|
||||||
<a>
|
<a title="Compute">
|
||||||
<Lucideicons.binary style={"color: #{hsl("compute", css: true)}"} aria-hidden="true" />
|
<.icon for="binary" color="auto" />
|
||||||
</a>
|
</a>
|
||||||
<a>
|
<a title="Storage">
|
||||||
<Lucideicons.hard_drive style={"color: #{hsl("storage", css: true)}"} aria-hidden="true" />
|
<.icon for="hard-drive" color="auto" />
|
||||||
</a>
|
</a>
|
||||||
<a>
|
<a title="Network">
|
||||||
<Lucideicons.earth style={"color: #{hsl("network", css: true)}"} aria-hidden="true" />
|
<.icon for="earth" color="auto" />
|
||||||
|
</a>
|
||||||
|
<a title="System">
|
||||||
|
<.icon for="bot" color="auto" />
|
||||||
</a>
|
</a>
|
||||||
<a><Lucideicons.bot style={"color: #{hsl("system", css: true)}"} aria-hidden="true" /></a>
|
|
||||||
</.panel_tabs>
|
</.panel_tabs>
|
||||||
|
|
||||||
<.panel_block>
|
<.panel_block>
|
||||||
|
@ -44,41 +45,23 @@ 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
|
|
||||||
style={"color: #{hsl("account settings", css: true)}"}
|
|
||||||
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">
|
||||||
<Lucideicons.puzzle
|
<.icon for="puzzle" color="auto" aria-hidden="true" />
|
||||||
style={"color: #{hsl("system services", css: true)}"}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<div class="mt-2 ml-2">System services</div>
|
<div class="mt-2 ml-2">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">
|
||||||
<Lucideicons.package
|
<.icon for="package" color="auto" aria-hidden="true" />
|
||||||
style={"color: #{hsl("system packages", css: true)}"}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-2 ml-2">System packages</span>
|
<span class="mt-2 ml-2">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">
|
||||||
<Lucideicons.hard_drive_download
|
<.icon for="hard-drive-download" color="auto" aria-hidden="true" />
|
||||||
style={"color: #{hsl("system software updates", css: true)}"}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<span class="mt-2 ml-2">System software updates</span>
|
<span class="mt-2 ml-2">Software updates</span>
|
||||||
</.link>
|
</.link>
|
||||||
</.panel>
|
</.panel>
|
||||||
</.section>
|
</.section>
|
||||||
|
|
|
@ -69,6 +69,7 @@ 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
|
||||||
|
|
24
lib/liliform.ex
Normal file
24
lib/liliform.ex
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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
|
23
lib/liliform/block.ex
Normal file
23
lib/liliform/block.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
23
lib/liliform/box.ex
Normal file
23
lib/liliform/box.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
35
lib/liliform/bulma_helper.ex
Normal file
35
lib/liliform/bulma_helper.ex
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
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
|
24
lib/liliform/button.ex
Normal file
24
lib/liliform/button.ex
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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
|
55
lib/liliform/colorhash.ex
Normal file
55
lib/liliform/colorhash.ex
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule Liliform.Colorhash do
|
||||||
|
@moduledoc """
|
||||||
|
Given a string returns HSL color.
|
||||||
|
"""
|
||||||
|
alias Freedive.Features
|
||||||
|
|
||||||
|
@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
|
||||||
|
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_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
|
23
lib/liliform/column.ex
Normal file
23
lib/liliform/column.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
17
lib/liliform/component.ex
Normal file
17
lib/liliform/component.ex
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
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
|
23
lib/liliform/container.ex
Normal file
23
lib/liliform/container.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
23
lib/liliform/content.ex
Normal file
23
lib/liliform/content.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
23
lib/liliform/control.ex
Normal file
23
lib/liliform/control.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
143
lib/liliform/flash.ex
Normal file
143
lib/liliform/flash.ex
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
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
|
23
lib/liliform/hero.ex
Normal file
23
lib/liliform/hero.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
119
lib/liliform/icon.ex
Normal file
119
lib/liliform/icon.ex
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
@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} color={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
|
||||||
|
case assigns.color do
|
||||||
|
nil -> ""
|
||||||
|
"auto" -> Colorhash.hsl(assigns.for)
|
||||||
|
color -> color
|
||||||
|
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
|
||||||
|
lucide_name -> String.to_atom(lucide_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
157
lib/liliform/input.ex
Normal file
157
lib/liliform/input.ex
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
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
|
19
lib/liliform/label.ex
Normal file
19
lib/liliform/label.ex
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
80
lib/liliform/media.ex
Normal file
80
lib/liliform/media.ex
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
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
|
209
lib/liliform/navbar.ex
Normal file
209
lib/liliform/navbar.ex
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
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
|
83
lib/liliform/panel.ex
Normal file
83
lib/liliform/panel.ex
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
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
|
23
lib/liliform/section.ex
Normal file
23
lib/liliform/section.ex
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
39
lib/liliform/simple_form.ex
Normal file
39
lib/liliform/simple_form.ex
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
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
|
43
lib/liliform/title.ex
Normal file
43
lib/liliform/title.ex
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
29
lib/liliform/translation.ex
Normal file
29
lib/liliform/translation.ex
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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
|
|
@ -1,23 +1,25 @@
|
||||||
defmodule Freedive.ColorhashTest do
|
defmodule Freedive.ColorhashTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
|
alias Liliform.Colorhash
|
||||||
|
alias Freedive.Features
|
||||||
|
|
||||||
test "hsl" do
|
test "hsl" do
|
||||||
assert Freedive.Colorhash.hsl("foo") == {312, 80, 48}
|
assert Colorhash.hsl("foo", raw: true) == {275, 85, 34}
|
||||||
assert Freedive.Colorhash.hsl("bar") == {38, 88, 38}
|
assert Colorhash.hsl("bar", raw: true) == {309, 92, 46}
|
||||||
assert Freedive.Colorhash.hsl("baz") == {288, 95, 42}
|
assert Colorhash.hsl("baz", raw: true) == {307, 87, 37}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "hsl with css" do
|
test "hsl with css" do
|
||||||
assert Freedive.Colorhash.hsl("foo", css: true) == "hsl(312, 80%, 48%)"
|
assert Colorhash.hsl("foo") == "hsl(275, 85%, 34%)"
|
||||||
assert Freedive.Colorhash.hsl("bar", css: true) == "hsl(38, 88%, 38%)"
|
assert Colorhash.hsl("bar") == "hsl(309, 92%, 46%)"
|
||||||
assert Freedive.Colorhash.hsl("baz", css: true) == "hsl(288, 95%, 42%)"
|
assert Colorhash.hsl("baz") == "hsl(307, 87%, 37%)"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "hsl with disabled feature" do
|
test "hsl with disabled feature" do
|
||||||
Freedive.Features.disable(:colorhash)
|
Features.disable(:colorhash)
|
||||||
assert Freedive.Colorhash.hsl("foo") == {0, 0, 0}
|
assert Colorhash.hsl("foo", raw: true) == {0, 0, 0}
|
||||||
assert Freedive.Colorhash.hsl("bar") == {0, 0, 0}
|
assert Colorhash.hsl("bar", raw: true) == {0, 0, 0}
|
||||||
assert Freedive.Colorhash.hsl("baz") == {0, 0, 0}
|
assert Colorhash.hsl("baz", raw: true) == {0, 0, 0}
|
||||||
Freedive.Features.enable(:colorhash)
|
Features.enable(:colorhash)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue