diff --git a/lib/freedive/colorhash.ex b/lib/freedive/colorhash.ex index da43237..b6c0a59 100644 --- a/lib/freedive/colorhash.ex +++ b/lib/freedive/colorhash.ex @@ -4,6 +4,8 @@ defmodule Freedive.Colorhash do """ alias Freedive.Features + @seed "Free_ive" + @default_hsl {0, 0, 0} def hsl(input, css: true) do @@ -13,7 +15,7 @@ defmodule Freedive.Colorhash do def hsl(input) do if Features.enabled?(:colorhash) do - input = String.downcase(input) + input = String.downcase(input) <> @seed hash = :erlang.phash2(input, 2_147_483_647) hue = calculate_hue(hash) @@ -35,7 +37,7 @@ defmodule Freedive.Colorhash do # Ranges to exclude (yellow to light green, cyan) excluded_ranges = [ # yellow - {50, 70}, + {40, 80}, # cyan {175, 185}, ] diff --git a/lib/freedive_web.ex b/lib/freedive_web.ex index c6c5928..223128b 100644 --- a/lib/freedive_web.ex +++ b/lib/freedive_web.ex @@ -53,7 +53,7 @@ defmodule FreediveWeb do quote do use Phoenix.LiveView, layout: {FreediveWeb.Layouts, :app}, - global_prefixes: ["is-", "has-", "flex-", "justify-", "align-"] + global_prefixes: ["is-", "has-"] unquote(html_helpers()) end @@ -69,7 +69,7 @@ defmodule FreediveWeb do def html 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 Phoenix.Controller, @@ -86,8 +86,9 @@ defmodule FreediveWeb do import Phoenix.HTML # Core UI components and translation import FreediveWeb.CoreComponents, only: [header: 1] - import Liliform.Components import FreediveWeb.Gettext + # Import custom components + use Liliform # Shortcut for generating JS commands alias Phoenix.LiveView.JS diff --git a/lib/freedive_web/components/layouts/app.html.heex b/lib/freedive_web/components/layouts/app.html.heex index 8088cb3..87dde96 100644 --- a/lib/freedive_web/components/layouts/app.html.heex +++ b/lib/freedive_web/components/layouts/app.html.heex @@ -12,45 +12,73 @@ <.navbar_menu id="navbar_top" class="mr-1"> - <%!-- <%= if @current_user do %> + <%= if @current_user do %> <.navbar_start> <.navbar_item has-dropdown is-hoverable> <.navbar_link> - Compute + <.icon for="compute" aria-hidden="true"> + Compute + <.navbar_dropdown> - <.navbar_item> - <.link>Bastille Jails - + <.link href={~p"/compute/apps"} class="navbar-item"> + Apps + + + <.link href={~p"/compute/tasks"} class="navbar-item"> + Tasks + <.navbar_item has-dropdown is-hoverable> <.navbar_link> - Storage + <.icon for="storage" aria-hidden="true"> + Storage + <.navbar_dropdown> - <.navbar_item> - <.link>Shared Folders - + <.link href={~p"/storage/local"} class="navbar-item"> + Local + + + <.link href={~p"/storage/shared"} class="navbar-item"> + Shared + + + <.link href={~p"/storage/remote"} class="navbar-item"> + Remote + <.navbar_item has-dropdown is-hoverable> <.navbar_link> - Network + <.icon for="network" aria-hidden="true"> + Network + <.navbar_dropdown> - <.navbar_item> - <.link>Private Network - + <.link href={~p"/network/public"} class="navbar-item"> + Public + + + <.link href={~p"/network/private"} class="navbar-item"> + Private + + + <.navbar_divider /> + + <.link href={~p"/network/endpoints"} class="navbar-item"> + Endpoints + - <% end %> --%> + <% end %> <.navbar_end> <%= if @current_user do %> @@ -60,30 +88,20 @@ <.navbar_item has-dropdown is-hoverable> <.navbar_link> - - - - System + <.icon for="system" aria-hidden="true"> + System + <.navbar_dropdown> <.link href={~p"/updates"} class="navbar-item"> - - - Software updates <.navbar_divider /> <.link href={~p"/packages"} class="navbar-item"> - - - Packages <.link href={~p"/services"} class="navbar-item"> - - - Services @@ -91,29 +109,22 @@ <% end %> <.navbar_item has-dropdown is-hoverable> <.navbar_link> - - - - Account + <.icon for="account" aria-hidden="true"> + Account + <.navbar_dropdown> <%= if @current_user do %> <.link href={~p"/users/settings"} class="navbar-item"> - - - Settings <.link href={~p"/users/log_out"} method="delete" class="navbar-item"> - - - Log out <% else %> <.link href={~p"/users/log_in"} class="navbar-item"> - Log in + Log in <% end %> diff --git a/lib/freedive_web/components/liliform.ex b/lib/freedive_web/components/liliform.ex deleted file mode 100644 index 63bc131..0000000 --- a/lib/freedive_web/components/liliform.ex +++ /dev/null @@ -1,1315 +0,0 @@ -defmodule Liliform.Components do - use Phoenix.Component, global_prefixes: ["is-", "has-", "flex-", "justify-", "align-"] - alias Phoenix.LiveView.JS - import FreediveWeb.Gettext - import PhxComponentHelpers - require Logger - - @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 - - @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 - assigns = - assigns - |> extend_class("label") - |> set_attributes([:for]) - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @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 `` 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""" - - - - - <%!-- todo: is focus:ring-0 part of tailwind? if yes, find alternative/remove --%> - <%= @label %> - - <.error :for={msg <- @errors}><%= msg %> - - """ - end - - def input(%{type: "select"} = assigns) do - ~H""" - - <.label for={@id}><%= @label %> - - <%= @prompt %> - <%= Phoenix.HTML.Form.options_for_select(@options, @value) %> - - <.error :for={msg <- @errors}><%= msg %> - - """ - end - - def input(%{type: "textarea"} = assigns) do - ~H""" - - <.label for={@id}><%= @label %> - <%= Phoenix.HTML.Form.normalize_value("textarea", @value) %> - <.error :for={msg <- @errors}><%= msg %> - - """ - end - - # All other inputs text, datetime-local, url, password, etc. are handled here... - def input(assigns) do - ~H""" - - <.label for={@id}><%= @label %> - - <.error :for={msg <- @errors}><%= msg %> - - """ - end - - @doc """ - Renders control. - """ - attr :class, :string, default: nil, doc: "the optional class of the control" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the control" - - slot :inner_block, required: true, doc: "the inner block that renders the control content" - - def control(assigns) do - assigns = - assigns - |> extend_class("control") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders section. - """ - attr :class, :string, default: nil, doc: "the optional class of the section" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the section" - - slot :inner_block, required: true, doc: "the inner block that renders the section content" - - def section(assigns) do - assigns = - assigns - |> extend_class("section") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders box. - """ - attr :class, :string, default: nil, doc: "the optional class of the box" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the box" - - slot :inner_block, required: true, doc: "the inner block that renders the box content" - - def box(assigns) do - assigns = - assigns - |> extend_class("box") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders block. - """ - attr :class, :string, default: nil, doc: "the optional class of the block" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the block" - - slot :inner_block, required: true, doc: "the inner block that renders the block content" - - def block(assigns) do - assigns = - assigns - |> extend_class("block") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders container. - """ - attr :class, :string, default: nil, doc: "the optional class of the container" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the container" - - slot :inner_block, required: true, doc: "the inner block that renders the container content" - - def container(assigns) do - assigns = - assigns - |> extend_class("container") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders footer. - """ - attr :class, :string, default: nil, doc: "the optional class of the footer" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the footer" - - slot :inner_block, required: true, doc: "the inner block that renders the footer content" - - def footer(assigns) do - assigns = - assigns - |> extend_class("footer") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - """ - end - - @doc """ - Renders media object. - """ - attr :class, :string, default: nil, doc: "the optional class of the media object" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the media object" - - slot :inner_block, required: true, doc: "the inner block that renders the media object content" - - def media(assigns) do - assigns = - assigns - |> extend_class("media") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders media-left. - """ - attr :class, :string, default: nil, doc: "the optional class of the media-left" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the media-left" - - slot :inner_block, required: true, doc: "the inner block that renders the media-left content" - - def media_left(assigns) do - assigns = - assigns - |> extend_class("media-left") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders media-content. - """ - attr :class, :string, default: nil, doc: "the optional class of the media-content" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the media-content" - - slot :inner_block, required: true, doc: "the inner block that renders the media-content content" - - def media_content(assigns) do - assigns = - assigns - |> extend_class("media-content") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders media-right. - """ - attr :class, :string, default: nil, doc: "the optional class of the media-right" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the media-right" - - slot :inner_block, required: true, doc: "the inner block that renders the media-right content" - - def media_right(assigns) do - assigns = - assigns - |> extend_class("media-right") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders content. - """ - attr :class, :string, default: nil, doc: "the optional class of the content" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the content" - - slot :inner_block, required: true, doc: "the inner block that renders the content" - - def content(assigns) do - assigns = - assigns - |> extend_class("content") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders a level. - """ - attr :class, :string, default: nil, doc: "the optional class of the level" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the level" - - slot :inner_block, required: true, doc: "the inner block that renders the level content" - - def level(assigns) do - assigns = - assigns - |> extend_class("level") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders level-left. - """ - attr :class, :string, default: nil, doc: "the optional class of the level-left" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the level-left" - - slot :inner_block, required: true, doc: "the inner block that renders the level-left content" - - def level_left(assigns) do - assigns = - assigns - |> extend_class("level-left") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders level-right. - """ - attr :class, :string, default: nil, doc: "the optional class of the level-right" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the level-right" - - slot :inner_block, required: true, doc: "the inner block that renders the level-right content" - - def level_right(assigns) do - assigns = - assigns - |> extend_class("level-right") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders level-item. - """ - attr :class, :string, default: nil, doc: "the optional class of the level-item" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the level-item" - - slot :inner_block, required: true, doc: "the inner block that renders the level-item content" - - def level_item(assigns) do - assigns = - assigns - |> extend_class("level-item") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders columns. - """ - attr :class, :string, default: nil, doc: "the optional class of the columns" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the column" - - slot :inner_block, required: true, doc: "the inner block that renders the columns content" - - def columns(assigns) do - assigns = - assigns - |> extend_class("columns") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders column. - """ - attr :class, :string, default: nil, doc: "the optional class of the column" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the column" - - slot :inner_block, required: true, doc: "the inner block that renders the column content" - - def column(assigns) do - assigns = - assigns - |> extend_class("column") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders button. - """ - attr :id, :string, required: false, doc: "the id of the button" - attr :type, :string, default: "button", doc: "the type of the button" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the button" - - slot :inner_block, required: true, doc: "the inner block that renders the button content" - - def button(assigns) do - assigns = - assigns - |> extend_class("button") - |> set_attributes([:type, :id]) - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders hero section. - """ - attr :id, :string, required: false, doc: "the id of the hero section" - attr :class, :string, default: nil, doc: "the optional class of the hero section" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the hero section" - - slot :inner_block, required: true, doc: "the inner block that renders the hero content" - - def hero(assigns) do - assigns = - assigns - |> extend_class("hero") - |> set_attributes([:id]) - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - - <%= render_slot(@inner_block) %> - - - """ - end - - @doc """ - Renders title. - """ - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the button" - - slot :inner_block, required: true, doc: "the inner block that renders the title content" - - def title(assigns) do - assigns - |> extend_class("title") - |> set_phx_attributes() - |> set_bulma_classes() - |> render_bulma_heading() - end - - @doc """ - Renders subtitle. - """ - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the button" - - slot :inner_block, required: true, doc: "the inner block that renders the subtitle content" - - def subtitle(assigns) do - assigns - |> extend_class("subtitle") - |> set_phx_attributes() - |> set_bulma_classes() - |> render_bulma_heading() - end - - @doc """ - Renders navbar. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar" - - slot :inner_block, required: true, doc: "the inner block that renders the navbar content" - - def navbar(assigns) do - assigns = - assigns - |> extend_class("navbar") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-brand. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar-brand" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-brand" - - slot :inner_block, required: true, doc: "the inner block that renders the navbar-brand content" - - def navbar_brand(assigns) do - assigns = - assigns - |> extend_class("navbar-brand") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-menu. - """ - attr :id, :string, required: true, doc: "the id of the navbar-menu" - attr :class, :string, default: nil, doc: "the optional class of the navbar-menu" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-menu" - - slot :inner_block, required: true, doc: "the inner block that renders the navbar-menu content" - - def navbar_menu(assigns) do - assigns = - assigns - |> extend_class("navbar-menu") - |> set_attributes([:id]) - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-start. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar-start" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-start" - - slot :inner_block, required: true, doc: "the inner block that renders the navbar-start content" - - def navbar_start(assigns) do - assigns = - assigns - |> extend_class("navbar-start") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-end. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar-end" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-end" - - slot :inner_block, required: true, doc: "the inner block that renders the navbar-end content" - - def navbar_end(assigns) do - assigns = - assigns - |> extend_class("navbar-end") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-item. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar-item" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-item" - - slot :inner_block, required: true, doc: "the inner block that renders the navbar-item content" - - def navbar_item(assigns) do - assigns = - assigns - |> extend_class("navbar-item") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-dropdown. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar-dropdown" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-dropdown" - - slot :inner_block, - required: true, - doc: "the inner block that renders the navbar-dropdown content" - - def navbar_dropdown(assigns) do - assigns = - assigns - |> extend_class("navbar-dropdown") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-link. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar-link" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-link" - - slot :inner_block, required: true, doc: "the inner block that renders the navbar-link content" - - def navbar_link(assigns) do - assigns = - assigns - |> extend_class("navbar-link") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders navbar-divider. - """ - attr :class, :string, default: nil, doc: "the optional class of the navbar-divider" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-divider" - - def navbar_divider(assigns) do - assigns = - assigns - |> extend_class("navbar-divider") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - """ - end - - @doc """ - Renders navbar-burger. - """ - attr :target, :string, required: true, doc: "the target of the navbar-burger" - attr :size, :string, default: "1.6rem", doc: "the size of the navbar-burger" - attr :class, :string, default: nil, doc: "the optional class of the navbar-burger" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the navbar-burger" - - def navbar_burger(assigns) do - assigns = - assigns - |> extend_class("navbar-burger") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - - - """ - end - - @doc """ - Renders a panel. - """ - attr :class, :string, default: nil, doc: "the optional class of the panel" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the panel" - - slot :inner_block, required: true, doc: "the inner block that renders the panel content" - - def panel(assigns) do - assigns = - assigns - |> extend_class("panel") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders a panel-heading. - """ - attr :class, :string, default: nil, doc: "the optional class of the panel-heading" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the panel-heading" - - slot :inner_block, required: true, doc: "the inner block that renders the panel-heading content" - - def panel_heading(assigns) do - assigns = - assigns - |> extend_class("panel-heading") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders a panel-tabs. - """ - attr :class, :string, default: nil, doc: "the optional class of the panel-tabs" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the panel-tabs" - - slot :inner_block, required: true, doc: "the inner block that renders the panel-tabs content" - - def panel_tabs(assigns) do - assigns = - assigns - |> extend_class("panel-tabs") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders a panel-block. - """ - attr :class, :string, default: nil, doc: "the optional class of the panel-block" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the panel-block" - - slot :inner_block, required: true, doc: "the inner block that renders the panel-block content" - - def panel_block(assigns) do - assigns = - assigns - |> extend_class("panel-block") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - <%= render_slot(@inner_block) %> - - """ - end - - @doc """ - Renders search input. - """ - attr :id, :string, required: true, doc: "the id of the search input" - attr :name, :string, required: true, doc: "the name of the search input" - attr :value, :string, default: nil, doc: "the value of the search input" - attr :placeholder, :string, default: nil, doc: "the placeholder of the search input" - attr :class, :string, default: nil, doc: "the optional class of the search input" - attr :rest, :global, doc: "the arbitrary HTML attributes to add to the search input" - - slot :inner_block, required: true, doc: "the inner block that renders the search input content" - slot :icon_left, doc: "the slot for the left icon" - slot :icon_right, doc: "the slot for the right icon" - - def search_input(assigns) do - assigns = - assigns - |> extend_class("control has-icons-left has-icons-right") - |> set_phx_attributes() - |> set_bulma_classes() - - ~H""" - - - <%= if @icon_left != [] do %> - - <%= render_slot(@icon_left) %> - - <% end %> - <%= if @icon_right != [] do %> - - <%= render_slot(@icon_right) %> - - <% end %> - - """ - end - - # @doc """ - # Renders a header with title. - # """ - # attr :class, :string, default: nil - - # slot :inner_block, required: true - # slot :subtitle - # slot :actions - - # def header(assigns) do - # ~H""" - # - # - # - # <%= render_slot(@inner_block) %> - # - # - # <%= render_slot(@subtitle) %> - # - # - # <%= render_slot(@actions) %> - # - # """ - # end - - @doc """ - Generates a generic error message. - """ - slot :inner_block, required: true - - def error(assigns) do - ~H""" - - <%!-- <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> --%> - - <%= render_slot(@inner_block) %> - - """ - end - - @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 - - - """ - 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}> - - <%= render_slot(@inner_block, f) %> - - <%= render_slot(action, f) %> - - - - """ - end - - @doc """ - Renders flash notices. - - ## Examples - - <.flash kind={:info} flash={@flash} /> - <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back! - """ - 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> - - - - <.media_content> - <.content> - <.title :if={@title} is-4> - <%= @title %> - - <.subtitle is-6> - <%= msg %> - - - - <.media_right is-hidden-touch> - <.button aria-label={gettext("close")} phx-click={hide("##{@id}")}> - - - - - - """ - 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""" - - <.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") %> - - - - <.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") %> - - - - """ - 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 - - ## Private functions - - defp set_bulma_classes(assigns, opts \\ []) do - opts = Keyword.put_new(opts, :from, :rest) - - assigns = - assigns - |> set_prefixed_attributes(["is-"], Keyword.put_new(opts, :into, :bulma_is)) - |> set_prefixed_attributes(["has-"], Keyword.put_new(opts, :into, :bulma_has)) - |> set_prefixed_attributes(["flex-"], Keyword.put_new(opts, :into, :bulma_flex)) - |> set_prefixed_attributes(["justify-"], Keyword.put_new(opts, :into, :bulma_justify)) - |> set_prefixed_attributes(["align-"], Keyword.put_new(opts, :into, :bulma_align)) - - bulma_classes = - [] - |> Enum.concat(for({is, _} <- assigns.heex_bulma_is, do: to_string(is))) - |> Enum.concat(for({has, _} <- assigns.heex_bulma_has, do: to_string(has))) - |> Enum.concat(for({flex, _} <- assigns.heex_bulma_flex, do: to_string(flex))) - |> Enum.concat(for({justify, _} <- assigns.heex_bulma_justify, do: to_string(justify))) - |> Enum.concat(for({align, _} <- assigns.heex_bulma_align, do: to_string(align))) - - if assigns.heex_class do - class = - assigns.heex_class[:class] - |> String.split(" ") - |> Enum.concat(bulma_classes) - # todo: where is this coming from? - |> Enum.reject(&(&1 == "false")) - |> Enum.join(" ") - - assigns |> Map.put(:heex_class, class: class) - else - assigns |> Map.put(:heex_class, class: bulma_classes |> Enum.join(" ")) - end - end - - slot :inner_block, required: true - - defp render_bulma_heading(assigns) do - level = - assigns - |> Map.filter(fn assign -> - case assign do - {:"is-1", _} -> true - {:"is-2", _} -> true - {:"is-3", _} -> true - {:"is-4", _} -> true - {:"is-5", _} -> true - {:"is-6", _} -> true - _ -> false - end - end) - |> Map.keys() - |> List.first() - |> Atom.to_string() - |> String.split("-") - |> List.last() - - level = - case level do - # todo: where is this coming from? - "nil" -> 3 - _ -> String.to_integer(level) - end - - case level do - 1 -> - ~H"<%= render_slot(@inner_block) %>" - - 2 -> - ~H"<%= render_slot(@inner_block) %>" - - 3 -> - ~H"<%= render_slot(@inner_block) %>" - - 4 -> - ~H"<%= render_slot(@inner_block) %>" - - 5 -> - ~H"<%= render_slot(@inner_block) %>" - - 6 -> - ~H"<%= render_slot(@inner_block) %>" - end - end -end diff --git a/lib/freedive_web/live/home_live.ex b/lib/freedive_web/live/home_live.ex index feef2b7..cd94797 100644 --- a/lib/freedive_web/live/home_live.ex +++ b/lib/freedive_web/live/home_live.ex @@ -1,6 +1,5 @@ defmodule FreediveWeb.HomeLive do use FreediveWeb, :live_view - import Freedive.Colorhash def render(assigns) do ~H""" @@ -15,19 +14,21 @@ defmodule FreediveWeb.HomeLive do System <.panel_tabs is-hidden-tablet> - - + + <.icon for="infinity" color="auto" /> - - + + <.icon for="binary" color="auto" /> - - + + <.icon for="hard-drive" color="auto" /> - - + + <.icon for="earth" color="auto" /> + + + <.icon for="bot" color="auto" /> - <.panel_block> @@ -44,41 +45,23 @@ defmodule FreediveWeb.HomeLive do - <.link patch={~p"/users/settings"} class="panel-block pt-1"> - - - - Account settings - <.link patch={~p"/services"} class="panel-block pt-1"> - + <.icon for="puzzle" color="auto" aria-hidden="true" /> - System services + Services <.link patch={~p"/packages"} class="panel-block pt-1"> - + <.icon for="package" color="auto" aria-hidden="true" /> - System packages + Packages <.link patch={~p"/updates"} class="panel-block pt-1"> - + <.icon for="hard-drive-download" color="auto" aria-hidden="true" /> - System software updates + Software updates diff --git a/lib/freedive_web/router.ex b/lib/freedive_web/router.ex index afa1895..12327fa 100644 --- a/lib/freedive_web/router.ex +++ b/lib/freedive_web/router.ex @@ -69,6 +69,7 @@ defmodule FreediveWeb.Router do live_session :require_authenticated_user, on_mount: [{FreediveWeb.UserAuth, :ensure_authenticated}] do live "/", HomeLive + live "/liliform", LiliformLive live "/users/settings", UserSettingsLive, :edit live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email end diff --git a/lib/liliform.ex b/lib/liliform.ex new file mode 100644 index 0000000..082027b --- /dev/null +++ b/lib/liliform.ex @@ -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 diff --git a/lib/liliform/block.ex b/lib/liliform/block.ex new file mode 100644 index 0000000..b753d16 --- /dev/null +++ b/lib/liliform/block.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/box.ex b/lib/liliform/box.ex new file mode 100644 index 0000000..1dbd749 --- /dev/null +++ b/lib/liliform/box.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/bulma_helper.ex b/lib/liliform/bulma_helper.ex new file mode 100644 index 0000000..bfb4b9f --- /dev/null +++ b/lib/liliform/bulma_helper.ex @@ -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 diff --git a/lib/liliform/button.ex b/lib/liliform/button.ex new file mode 100644 index 0000000..6248b19 --- /dev/null +++ b/lib/liliform/button.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/colorhash.ex b/lib/liliform/colorhash.ex new file mode 100644 index 0000000..3f5c476 --- /dev/null +++ b/lib/liliform/colorhash.ex @@ -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 diff --git a/lib/liliform/column.ex b/lib/liliform/column.ex new file mode 100644 index 0000000..8291d5c --- /dev/null +++ b/lib/liliform/column.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/component.ex b/lib/liliform/component.ex new file mode 100644 index 0000000..756e548 --- /dev/null +++ b/lib/liliform/component.ex @@ -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 diff --git a/lib/liliform/container.ex b/lib/liliform/container.ex new file mode 100644 index 0000000..e88ee13 --- /dev/null +++ b/lib/liliform/container.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/content.ex b/lib/liliform/content.ex new file mode 100644 index 0000000..226bfcf --- /dev/null +++ b/lib/liliform/content.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/control.ex b/lib/liliform/control.ex new file mode 100644 index 0000000..7a4e6df --- /dev/null +++ b/lib/liliform/control.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/flash.ex b/lib/liliform/flash.ex new file mode 100644 index 0000000..ba5928c --- /dev/null +++ b/lib/liliform/flash.ex @@ -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! + """ + 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> + + + + <.media_content> + <.content> + <.title :if={@title} is-4> + <%= @title %> + + <.subtitle is-6> + <%= msg %> + + + + <.media_right is-hidden-touch> + <.button aria-label={gettext("close")} phx-click={hide("##{@id}")}> + + + + + + """ + 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""" + + <.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") %> + + + + <.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") %> + + + + """ + 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 diff --git a/lib/liliform/hero.ex b/lib/liliform/hero.ex new file mode 100644 index 0000000..f0af208 --- /dev/null +++ b/lib/liliform/hero.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/icon.ex b/lib/liliform/icon.ex new file mode 100644 index 0000000..252524c --- /dev/null +++ b/lib/liliform/icon.ex @@ -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""" + + + <.icon_svg for={@for} height={@size} width={@size} color={icon_color(assigns)} /> + + <%= if @inner_block != [] do %> + + <%= render_slot(@inner_block) %> + + <% end %> + + """ + 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 diff --git a/lib/liliform/input.ex b/lib/liliform/input.ex new file mode 100644 index 0000000..2328e37 --- /dev/null +++ b/lib/liliform/input.ex @@ -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 `` 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""" + + + + + <%= @label %> + + <.error :for={msg <- @errors}><%= msg %> + + """ + end + + def input(%{type: "select"} = assigns) do + ~H""" + + <.label for={@id}><%= @label %> + + <%= @prompt %> + <%= Phoenix.HTML.Form.options_for_select(@options, @value) %> + + <.error :for={msg <- @errors}><%= msg %> + + """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" + + <.label for={@id}><%= @label %> + <%= Phoenix.HTML.Form.normalize_value("textarea", @value) %> + <.error :for={msg <- @errors}><%= msg %> + + """ + end + + # All other inputs text, datetime-local, url, password, etc. are handled here... + def input(assigns) do + ~H""" + + <.label for={@id}><%= @label %> + + <.error :for={msg <- @errors}><%= msg %> + + """ + end + + @doc """ + Generates a generic error message. + """ + slot :inner_block, required: true + + def error(assigns) do + ~H""" + + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/label.ex b/lib/liliform/label.ex new file mode 100644 index 0000000..3d97a18 --- /dev/null +++ b/lib/liliform/label.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/media.ex b/lib/liliform/media.ex new file mode 100644 index 0000000..629fab9 --- /dev/null +++ b/lib/liliform/media.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/navbar.ex b/lib/liliform/navbar.ex new file mode 100644 index 0000000..f8c6c6d --- /dev/null +++ b/lib/liliform/navbar.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + """ + 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""" + + + + + + + """ + end +end diff --git a/lib/liliform/panel.ex b/lib/liliform/panel.ex new file mode 100644 index 0000000..5b9ba71 --- /dev/null +++ b/lib/liliform/panel.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/section.ex b/lib/liliform/section.ex new file mode 100644 index 0000000..6b84e08 --- /dev/null +++ b/lib/liliform/section.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/simple_form.ex b/lib/liliform/simple_form.ex new file mode 100644 index 0000000..8db1134 --- /dev/null +++ b/lib/liliform/simple_form.ex @@ -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 + + + """ + 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}> + + <%= render_slot(@inner_block, f) %> + + <%= render_slot(action, f) %> + + + + """ + end +end diff --git a/lib/liliform/title.ex b/lib/liliform/title.ex new file mode 100644 index 0000000..010c62f --- /dev/null +++ b/lib/liliform/title.ex @@ -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""" + + <%= render_slot(@inner_block) %> + + """ + 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""" + + <%= render_slot(@inner_block) %> + + """ + end +end diff --git a/lib/liliform/translation.ex b/lib/liliform/translation.ex new file mode 100644 index 0000000..2d57e3d --- /dev/null +++ b/lib/liliform/translation.ex @@ -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 diff --git a/test/freedive/colorhash_test.exs b/test/freedive/colorhash_test.exs index 532e5ad..b1a8e13 100644 --- a/test/freedive/colorhash_test.exs +++ b/test/freedive/colorhash_test.exs @@ -1,23 +1,25 @@ defmodule Freedive.ColorhashTest do use ExUnit.Case + alias Liliform.Colorhash + alias Freedive.Features test "hsl" do - assert Freedive.Colorhash.hsl("foo") == {312, 80, 48} - assert Freedive.Colorhash.hsl("bar") == {38, 88, 38} - assert Freedive.Colorhash.hsl("baz") == {288, 95, 42} + 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 Freedive.Colorhash.hsl("foo", css: true) == "hsl(312, 80%, 48%)" - assert Freedive.Colorhash.hsl("bar", css: true) == "hsl(38, 88%, 38%)" - assert Freedive.Colorhash.hsl("baz", css: true) == "hsl(288, 95%, 42%)" + 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 - Freedive.Features.disable(:colorhash) - assert Freedive.Colorhash.hsl("foo") == {0, 0, 0} - assert Freedive.Colorhash.hsl("bar") == {0, 0, 0} - assert Freedive.Colorhash.hsl("baz") == {0, 0, 0} - Freedive.Features.enable(:colorhash) + 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
- <%= render_slot(@inner_block) %> -
- # <%= render_slot(@subtitle) %> - #
- <%!-- <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> --%> - - <%= render_slot(@inner_block) %> -
+ + <%= render_slot(@inner_block) %> +
+ <%= render_slot(@inner_block) %> +