From c497aa65d408c34e4edd612fc3b0fb40c46e315a Mon Sep 17 00:00:00 2001 From: TheFirstAvenger Date: Fri, 15 Nov 2024 13:45:19 -0500 Subject: [PATCH 1/4] Move components and update CoreComponents handling --- lib/mix/tasks/station_ui.install.ex | 33 ++-- .../station_ui/HTML/accordion.ex | 0 .../station_ui/HTML/avatars.ex | 0 .../station_ui/HTML/banners.ex | 0 .../station_ui/HTML/buttons.ex | 0 .../{ => components}/station_ui/HTML/cards.ex | 0 .../station_ui/HTML/footer.ex | 0 .../{ => components}/station_ui/HTML/forms.ex | 0 .../{ => components}/station_ui/HTML/icons.ex | 0 .../station_ui/HTML/inputs.ex | 0 .../station_ui/HTML/legacy_core_components.ex | 0 .../station_ui/HTML/modals.ex | 0 .../station_ui/HTML/navbar.ex | 0 .../station_ui/HTML/notification_badges.ex | 0 .../station_ui/HTML/pagination.ex | 0 .../station_ui/HTML/spinners.ex | 0 .../station_ui/HTML/status_badges.ex | 0 .../station_ui/HTML/tab_group.ex | 0 .../station_ui/HTML/table_cell.ex | 0 .../station_ui/HTML/table_header.ex | 0 .../{ => components}/station_ui/HTML/tags.ex | 0 .../{ => components}/station_ui/HTML/toast.ex | 0 .../station_ui/HTML/toolbars.ex | 0 .../station_ui/HTML/tooltips.ex | 0 .../station_ui/station_ui.html.ex} | 0 sources/lib/station_ui/HTML/icon_buttons.ex | 144 ------------------ 26 files changed, 19 insertions(+), 158 deletions(-) rename sources/lib/{ => components}/station_ui/HTML/accordion.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/avatars.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/banners.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/buttons.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/cards.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/footer.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/forms.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/icons.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/inputs.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/legacy_core_components.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/modals.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/navbar.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/notification_badges.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/pagination.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/spinners.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/status_badges.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/tab_group.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/table_cell.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/table_header.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/tags.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/toast.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/toolbars.ex (100%) rename sources/lib/{ => components}/station_ui/HTML/tooltips.ex (100%) rename sources/lib/{station_ui/HTML.ex => components/station_ui/station_ui.html.ex} (100%) delete mode 100644 sources/lib/station_ui/HTML/icon_buttons.ex diff --git a/lib/mix/tasks/station_ui.install.ex b/lib/mix/tasks/station_ui.install.ex index 7673a7f..6062012 100644 --- a/lib/mix/tasks/station_ui.install.ex +++ b/lib/mix/tasks/station_ui.install.ex @@ -6,7 +6,7 @@ defmodule Mix.Tasks.StationUi.Install do @assets_subpath "assets" - @station_ui_subpath "station_ui" + @station_ui_subpath "components/station_ui" @sources_root_path __DIR__ |> Path.join("../../../sources") |> Path.expand() @@ -131,7 +131,7 @@ defmodule Mix.Tasks.StationUi.Install do end defp template_source_destination_file_path(context, source_file_path) do - installer_lib = Path.join(@sources_root_path, "lib/station_ui") + installer_lib = Path.join(@sources_root_path, "lib/components/station_ui") relative_source_path = Path.relative_to(source_file_path, installer_lib) Path.join([context.web_path, @station_ui_subpath, relative_source_path]) @@ -205,27 +205,32 @@ defmodule Mix.Tasks.StationUi.Install do web_app_ex_path = PathContext.web_app_ex_path(context) web_app_ex_content = File.read!(web_app_ex_path) + core_components_import = "import #{context.web_app_name}.CoreComponents" web_app_ex_content = String.replace( web_app_ex_content, - "import #{context.web_app_name}.CoreComponents", - "use #{context.web_app_name}.StationUI.HTML" + core_components_import, + "# #{core_components_import}\n use #{context.web_app_name}.StationUI.HTML" ) File.write!(web_app_ex_path, web_app_ex_content) IO.puts("Updated #{web_app_ex_path}") - core_components_path = Path.join(context.web_path, "components/core_components.ex") - [first | rest] = File.read!(core_components_path) |> String.trim() |> String.split("\n") - last = List.last(rest) - File.rm!(core_components_path) - - File.write( - core_components_path, - Enum.join([first, " # Replaced by StationUI components", last], "\n") - ) + if Mix.shell().yes?( + "StationUI is designed to replace the default Phoenix CoreComponents. Ok to clear out the existing CoreComponents file? Unless you made custom changes to this file that you still want, this should be fine to do." + ) do + core_components_path = Path.join(context.web_path, "components/core_components.ex") + [first | rest] = File.read!(core_components_path) |> String.trim() |> String.split("\n") + last = List.last(rest) + File.rm!(core_components_path) + + File.write( + core_components_path, + Enum.join([first, " # Replaced by StationUI components, do not remove or phoenix generators will recreate the full file", last], "\n") + ) - IO.puts("Replaced #{core_components_path}") + IO.puts("Replaced #{core_components_path}") + end end end diff --git a/sources/lib/station_ui/HTML/accordion.ex b/sources/lib/components/station_ui/HTML/accordion.ex similarity index 100% rename from sources/lib/station_ui/HTML/accordion.ex rename to sources/lib/components/station_ui/HTML/accordion.ex diff --git a/sources/lib/station_ui/HTML/avatars.ex b/sources/lib/components/station_ui/HTML/avatars.ex similarity index 100% rename from sources/lib/station_ui/HTML/avatars.ex rename to sources/lib/components/station_ui/HTML/avatars.ex diff --git a/sources/lib/station_ui/HTML/banners.ex b/sources/lib/components/station_ui/HTML/banners.ex similarity index 100% rename from sources/lib/station_ui/HTML/banners.ex rename to sources/lib/components/station_ui/HTML/banners.ex diff --git a/sources/lib/station_ui/HTML/buttons.ex b/sources/lib/components/station_ui/HTML/buttons.ex similarity index 100% rename from sources/lib/station_ui/HTML/buttons.ex rename to sources/lib/components/station_ui/HTML/buttons.ex diff --git a/sources/lib/station_ui/HTML/cards.ex b/sources/lib/components/station_ui/HTML/cards.ex similarity index 100% rename from sources/lib/station_ui/HTML/cards.ex rename to sources/lib/components/station_ui/HTML/cards.ex diff --git a/sources/lib/station_ui/HTML/footer.ex b/sources/lib/components/station_ui/HTML/footer.ex similarity index 100% rename from sources/lib/station_ui/HTML/footer.ex rename to sources/lib/components/station_ui/HTML/footer.ex diff --git a/sources/lib/station_ui/HTML/forms.ex b/sources/lib/components/station_ui/HTML/forms.ex similarity index 100% rename from sources/lib/station_ui/HTML/forms.ex rename to sources/lib/components/station_ui/HTML/forms.ex diff --git a/sources/lib/station_ui/HTML/icons.ex b/sources/lib/components/station_ui/HTML/icons.ex similarity index 100% rename from sources/lib/station_ui/HTML/icons.ex rename to sources/lib/components/station_ui/HTML/icons.ex diff --git a/sources/lib/station_ui/HTML/inputs.ex b/sources/lib/components/station_ui/HTML/inputs.ex similarity index 100% rename from sources/lib/station_ui/HTML/inputs.ex rename to sources/lib/components/station_ui/HTML/inputs.ex diff --git a/sources/lib/station_ui/HTML/legacy_core_components.ex b/sources/lib/components/station_ui/HTML/legacy_core_components.ex similarity index 100% rename from sources/lib/station_ui/HTML/legacy_core_components.ex rename to sources/lib/components/station_ui/HTML/legacy_core_components.ex diff --git a/sources/lib/station_ui/HTML/modals.ex b/sources/lib/components/station_ui/HTML/modals.ex similarity index 100% rename from sources/lib/station_ui/HTML/modals.ex rename to sources/lib/components/station_ui/HTML/modals.ex diff --git a/sources/lib/station_ui/HTML/navbar.ex b/sources/lib/components/station_ui/HTML/navbar.ex similarity index 100% rename from sources/lib/station_ui/HTML/navbar.ex rename to sources/lib/components/station_ui/HTML/navbar.ex diff --git a/sources/lib/station_ui/HTML/notification_badges.ex b/sources/lib/components/station_ui/HTML/notification_badges.ex similarity index 100% rename from sources/lib/station_ui/HTML/notification_badges.ex rename to sources/lib/components/station_ui/HTML/notification_badges.ex diff --git a/sources/lib/station_ui/HTML/pagination.ex b/sources/lib/components/station_ui/HTML/pagination.ex similarity index 100% rename from sources/lib/station_ui/HTML/pagination.ex rename to sources/lib/components/station_ui/HTML/pagination.ex diff --git a/sources/lib/station_ui/HTML/spinners.ex b/sources/lib/components/station_ui/HTML/spinners.ex similarity index 100% rename from sources/lib/station_ui/HTML/spinners.ex rename to sources/lib/components/station_ui/HTML/spinners.ex diff --git a/sources/lib/station_ui/HTML/status_badges.ex b/sources/lib/components/station_ui/HTML/status_badges.ex similarity index 100% rename from sources/lib/station_ui/HTML/status_badges.ex rename to sources/lib/components/station_ui/HTML/status_badges.ex diff --git a/sources/lib/station_ui/HTML/tab_group.ex b/sources/lib/components/station_ui/HTML/tab_group.ex similarity index 100% rename from sources/lib/station_ui/HTML/tab_group.ex rename to sources/lib/components/station_ui/HTML/tab_group.ex diff --git a/sources/lib/station_ui/HTML/table_cell.ex b/sources/lib/components/station_ui/HTML/table_cell.ex similarity index 100% rename from sources/lib/station_ui/HTML/table_cell.ex rename to sources/lib/components/station_ui/HTML/table_cell.ex diff --git a/sources/lib/station_ui/HTML/table_header.ex b/sources/lib/components/station_ui/HTML/table_header.ex similarity index 100% rename from sources/lib/station_ui/HTML/table_header.ex rename to sources/lib/components/station_ui/HTML/table_header.ex diff --git a/sources/lib/station_ui/HTML/tags.ex b/sources/lib/components/station_ui/HTML/tags.ex similarity index 100% rename from sources/lib/station_ui/HTML/tags.ex rename to sources/lib/components/station_ui/HTML/tags.ex diff --git a/sources/lib/station_ui/HTML/toast.ex b/sources/lib/components/station_ui/HTML/toast.ex similarity index 100% rename from sources/lib/station_ui/HTML/toast.ex rename to sources/lib/components/station_ui/HTML/toast.ex diff --git a/sources/lib/station_ui/HTML/toolbars.ex b/sources/lib/components/station_ui/HTML/toolbars.ex similarity index 100% rename from sources/lib/station_ui/HTML/toolbars.ex rename to sources/lib/components/station_ui/HTML/toolbars.ex diff --git a/sources/lib/station_ui/HTML/tooltips.ex b/sources/lib/components/station_ui/HTML/tooltips.ex similarity index 100% rename from sources/lib/station_ui/HTML/tooltips.ex rename to sources/lib/components/station_ui/HTML/tooltips.ex diff --git a/sources/lib/station_ui/HTML.ex b/sources/lib/components/station_ui/station_ui.html.ex similarity index 100% rename from sources/lib/station_ui/HTML.ex rename to sources/lib/components/station_ui/station_ui.html.ex diff --git a/sources/lib/station_ui/HTML/icon_buttons.ex b/sources/lib/station_ui/HTML/icon_buttons.ex deleted file mode 100644 index b265803..0000000 --- a/sources/lib/station_ui/HTML/icon_buttons.ex +++ /dev/null @@ -1,144 +0,0 @@ -defmodule StationUI.HTML.IconButtons do - use Phoenix.Component - - import StationUI.HTML.Icons, only: [icon: 1] - - @button_base [ - "bg-skin-btn text-skin-btn rounded-full border border-skin-btn whitespace-nowrap inline-flex justify-center items-center gap-x-1.5 lg:gap-x-2", - "hover:bg-skin-btn-hover active:bg-skin-btn-active hover:text-skin-btn-hover active:text-skin-btn-active hover:border-skin-btn-hover active:border-skin-btn-active focus-visible:ring-purple-500 focus-visible:ring-offset-4 focus-visible:ring-2 focus-visible:outline-none" - ] - - @destructive_classes "!border-skin-btn-destructive !bg-skin-btn-destructive text-skin-btn-destructive hover:!bg-skin-btn-destructive-hover hover:!border-skin-btn-destructive-hover hover:!text-skin-btn-destructive active:!bg-skin-btn-destructive-active active:!border-skin-btn-destructive-active active:!text-skin-btn-destructive" - - attr :rest, :global - attr :flavor, :string, default: "primary" - attr :destructive, :boolean, default: false - attr :size, :string, default: "md" - attr :responsive, :boolean, default: true - attr :pill, :boolean, default: false - attr :class, :string, default: nil - attr :icon, :string, required: true - attr :chevron, :boolean, default: nil - # We need to require one or the other of these to exist: - attr :text, :string, default: "" - attr :label, :string, default: "" - - def icon_btn(assigns) do - if assigns.text == "" and assigns.label == "", - do: raise("buttons without text require a label") - - ~H""" - - """ - end - - defp button_base, do: @button_base - - # Button flavor specific classes classes - - defp button_flavor("primary", destructive) do - if destructive, do: "btn-primary #{@destructive_classes}", else: "btn-primary" - end - - defp button_flavor("secondary", destructive) do - if destructive, do: "btn-secondary #{@destructive_classes}", else: "btn-secondary" - end - - defp button_flavor("tertiary", _), do: "btn-tertiary" - - # Size variant specific classes classes - # button_size(size, responsive, chevron, text) - - ## xl without label - defp button_size("xl", true, nil, ""), do: "lg:focus-visible:ring-4 p-[18px]" - defp button_size("xl", false, nil, ""), do: "text-base lg:focus-visible:ring-4 p-[18px]" - defp button_size("xl", true, _, ""), do: "lg:focus-visible:ring-4 py-[18px] pl-[22px] pr-[18px]" - - defp button_size("xl", false, _, ""), - do: "text-base lg:focus-visible:ring-4 py-[18px] pl-[22px] pr-[18px]" - - ## xl with label - defp button_size("xl", true, nil, _), do: "lg:focus-visible:ring-4 py-3 px-6" - defp button_size("xl", false, nil, _), do: "text-base lg:focus-visible:ring-4 py-3 px-6" - defp button_size("xl", true, _, _), do: "lg:focus-visible:ring-4 py-3 pl-7 pr-6" - defp button_size("xl", false, _, _), do: "text-base lg:focus-visible:ring-4 py-3 pl-7 pr-6" - - ## lg without label - defp button_size("lg", true, nil, ""), do: "text-base p-3.5" - defp button_size("lg", false, nil, ""), do: "text-base p-3.5" - defp button_size("lg", true, _, ""), do: "text-base py-3.5 pl-[18px] pr-3.5" - defp button_size("lg", false, _, ""), do: "text-base py-3.5 pl-[18px] pr-3.5" - - ## lg with label - defp button_size("lg", true, nil, _), do: "text-base py-2.5 px-5" - defp button_size("lg", false, nil, _), do: "text-base py-2.5 px-5" - defp button_size("lg", true, _, _), do: "text-base py-2.5 pl-6 pr-5" - defp button_size("lg", false, _, _), do: "text-base py-2.5 pl-6 pr-5" - - ## md without label - defp button_size("md", true, nil, ""), do: "text-sm p-2.5" - defp button_size("md", false, nil, ""), do: "text-sm p-2.5" - defp button_size("md", true, _, ""), do: "text-sm py-2.5 pl-3.5 pr-2.5" - defp button_size("md", false, _, ""), do: "text-sm py-2.5 pl-3.5 pr-2.5" - - ## md with label - defp button_size("md", true, nil, _), do: "text-sm py-2 px-3.5" - defp button_size("md", false, nil, _), do: "text-sm py-2 px-3.5" - defp button_size("md", true, _, _), do: "text-sm py-2 pl-[22px] pr-[18px]" - defp button_size("md", false, _, _), do: "text-sm py-2 pl-[22px] pr-[18px]" - - ## sm without label - defp button_size("sm", true, nil, ""), do: "text-xs p-2" - defp button_size("sm", false, nil, ""), do: "text-xs p-2" - defp button_size("sm", true, _, ""), do: "text-xs py-2 pl-3 pr-2" - defp button_size("sm", false, _, ""), do: "text-xs py-2 pl-3 pr-2" - - ## sm with label - defp button_size("sm", true, nil, _), do: "text-xs py-1.5 px-3" - defp button_size("sm", false, nil, _), do: "text-xs py-1.5 px-3" - defp button_size("sm", true, _, _), do: "text-xs py-1.5 pl-[18px] pr-3.5" - defp button_size("sm", false, _, _), do: "text-xs py-1.5 pl-[18px] pr-3.5" - - # Icon classes - - defp icon_classes("xl", true), do: "w-10 h-10 lg:w-12 lg:h-12" - defp icon_classes("xl", false), do: "w-12 h-12" - - defp icon_classes("lg", true), do: "w-8 h-8 lg:w-[38px] lg:h-[38px]" - defp icon_classes("lg", false), do: "w-[38px] h-[38px]" - - defp icon_classes("md", true), do: "w-6 h-6 lg:w-8 lg:h-8" - defp icon_classes("md", false), do: "w-8 h-8" - - defp icon_classes("sm", true), do: "w-6 h-6" - defp icon_classes("sm", false), do: "w-6 h-6" - - # Chevron classes - - defp chevron_classes("xl", true), do: "w-6 h-6 lg:w-7 lg:h-7" - defp chevron_classes("xl", false), do: "w-7 h-7" - - defp chevron_classes("lg", true), do: "w-6 h-6 lg:w-7 lg:h-7" - defp chevron_classes("lg", false), do: "w-7 h-7" - - defp chevron_classes("md", true), do: "w-5 h-5 lg:w-6 lg:h-6" - defp chevron_classes("md", false), do: "stroke stroke-white w-6 h-6" - - defp chevron_classes("sm", true), do: "w-4 h-4" - defp chevron_classes("sm", false), do: "w-4 h-4" -end From 598a5253fc835c4ada7c3e82cd9941806b06fec7 Mon Sep 17 00:00:00 2001 From: TheFirstAvenger Date: Fri, 15 Nov 2024 14:00:09 -0500 Subject: [PATCH 2/4] IO.puts to Mix.shell().info() --- lib/mix/tasks/station_ui.install.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mix/tasks/station_ui.install.ex b/lib/mix/tasks/station_ui.install.ex index 6062012..5f1e3af 100644 --- a/lib/mix/tasks/station_ui.install.ex +++ b/lib/mix/tasks/station_ui.install.ex @@ -191,7 +191,7 @@ defmodule Mix.Tasks.StationUi.Install do String.replace(tailwind_content, module_exports_line, module_exports_line <> sui_preset) File.write!(tailwind_config_path, tailwind_content) - IO.puts("Updated #{tailwind_config_path}") + Mix.shell().info("Updated #{tailwind_config_path}") app_css_path = Path.join(context.web_assets_path, "css/app.css") app_css_content = File.read!(app_css_path) @@ -201,7 +201,7 @@ defmodule Mix.Tasks.StationUi.Install do ~s|@import "./station-ui.css";\n@import "./station-ui-fonts.css";\n| <> app_css_content ) - IO.puts("Updated #{app_css_path}") + Mix.shell().info("Updated #{app_css_path}") web_app_ex_path = PathContext.web_app_ex_path(context) web_app_ex_content = File.read!(web_app_ex_path) @@ -215,7 +215,7 @@ defmodule Mix.Tasks.StationUi.Install do ) File.write!(web_app_ex_path, web_app_ex_content) - IO.puts("Updated #{web_app_ex_path}") + Mix.shell().info("Updated #{web_app_ex_path}") if Mix.shell().yes?( "StationUI is designed to replace the default Phoenix CoreComponents. Ok to clear out the existing CoreComponents file? Unless you made custom changes to this file that you still want, this should be fine to do." @@ -230,7 +230,7 @@ defmodule Mix.Tasks.StationUi.Install do Enum.join([first, " # Replaced by StationUI components, do not remove or phoenix generators will recreate the full file", last], "\n") ) - IO.puts("Replaced #{core_components_path}") + Mix.shell().info("Replaced #{core_components_path}") end end end From 1f1fe72f9f3e6e19129a466ed26c069507298c31 Mon Sep 17 00:00:00 2001 From: TheFirstAvenger Date: Fri, 15 Nov 2024 14:02:32 -0500 Subject: [PATCH 3/4] Add output when skipping CoreComponents replacement --- lib/mix/tasks/station_ui.install.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/mix/tasks/station_ui.install.ex b/lib/mix/tasks/station_ui.install.ex index 5f1e3af..ce7c130 100644 --- a/lib/mix/tasks/station_ui.install.ex +++ b/lib/mix/tasks/station_ui.install.ex @@ -231,6 +231,8 @@ defmodule Mix.Tasks.StationUi.Install do ) Mix.shell().info("Replaced #{core_components_path}") + else + Mix.shell().info("Skipping replacing CoreComponents") end end end From 43185056e0b3d5604532485379ca6a65c575e16a Mon Sep 17 00:00:00 2001 From: TheFirstAvenger Date: Fri, 15 Nov 2024 14:47:14 -0500 Subject: [PATCH 4/4] HTML folder to html --- .../components/station_ui/html/accordion.ex | 139 +++ .../lib/components/station_ui/html/avatars.ex | 239 ++++ .../lib/components/station_ui/html/banners.ex | 72 ++ .../lib/components/station_ui/html/buttons.ex | 84 ++ .../lib/components/station_ui/html/cards.ex | 180 +++ .../lib/components/station_ui/html/footer.ex | 224 ++++ .../lib/components/station_ui/html/forms.ex | 100 ++ .../lib/components/station_ui/html/icons.ex | 28 + .../lib/components/station_ui/html/inputs.ex | 1026 +++++++++++++++++ .../station_ui/html/legacy_core_components.ex | 300 +++++ .../lib/components/station_ui/html/modals.ex | 162 +++ .../lib/components/station_ui/html/navbar.ex | 102 ++ .../station_ui/html/notification_badges.ex | 35 + .../components/station_ui/html/pagination.ex | 224 ++++ .../components/station_ui/html/spinners.ex | 42 + .../station_ui/html/status_badges.ex | 38 + .../components/station_ui/html/tab_group.ex | 150 +++ .../components/station_ui/html/table_cell.ex | 44 + .../station_ui/html/table_header.ex | 44 + .../lib/components/station_ui/html/tags.ex | 74 ++ .../lib/components/station_ui/html/toast.ex | 68 ++ .../components/station_ui/html/toolbars.ex | 128 ++ .../components/station_ui/html/tooltips.ex | 34 + 23 files changed, 3537 insertions(+) create mode 100644 sources/lib/components/station_ui/html/accordion.ex create mode 100644 sources/lib/components/station_ui/html/avatars.ex create mode 100644 sources/lib/components/station_ui/html/banners.ex create mode 100644 sources/lib/components/station_ui/html/buttons.ex create mode 100644 sources/lib/components/station_ui/html/cards.ex create mode 100644 sources/lib/components/station_ui/html/footer.ex create mode 100644 sources/lib/components/station_ui/html/forms.ex create mode 100644 sources/lib/components/station_ui/html/icons.ex create mode 100644 sources/lib/components/station_ui/html/inputs.ex create mode 100644 sources/lib/components/station_ui/html/legacy_core_components.ex create mode 100644 sources/lib/components/station_ui/html/modals.ex create mode 100644 sources/lib/components/station_ui/html/navbar.ex create mode 100644 sources/lib/components/station_ui/html/notification_badges.ex create mode 100644 sources/lib/components/station_ui/html/pagination.ex create mode 100644 sources/lib/components/station_ui/html/spinners.ex create mode 100644 sources/lib/components/station_ui/html/status_badges.ex create mode 100644 sources/lib/components/station_ui/html/tab_group.ex create mode 100644 sources/lib/components/station_ui/html/table_cell.ex create mode 100644 sources/lib/components/station_ui/html/table_header.ex create mode 100644 sources/lib/components/station_ui/html/tags.ex create mode 100644 sources/lib/components/station_ui/html/toast.ex create mode 100644 sources/lib/components/station_ui/html/toolbars.ex create mode 100644 sources/lib/components/station_ui/html/tooltips.ex diff --git a/sources/lib/components/station_ui/html/accordion.ex b/sources/lib/components/station_ui/html/accordion.ex new file mode 100644 index 0000000..8a2f888 --- /dev/null +++ b/sources/lib/components/station_ui/html/accordion.ex @@ -0,0 +1,139 @@ +defmodule StationUI.HTML.Accordion do + use Phoenix.Component + + import StationUI.HTML.Icons, only: [icon: 1] + alias Phoenix.LiveView.JS + + @moduledoc """ + The accordion component renders a list of items with child content that can be expanded or collapsed. + + ## Example + + <.accordion_set> + <:header> + Title something 1 + + <:content> + Content something 1 + + + + Suggested size classes + + The Default size for accordions is "md" but the size can be change by passing in these additional classes + using `header_size_class="..."` and `content_size_class="..."` as follows + + header_size_class: + + sm: "p-1 text-base sm:text-lg gap-x-0.5" + md: "p-1 text-base sm:text-lg md:text-xl md:py-1 md:pr-1 md:pl-1.5 md:gap-x-1" + lg: "p-1 text-base sm:text-lg md:text-xl lg:text-2xl md:py-1 md:pr-1 md:pl-1.5 lg:pl-2 md:gap-x-1 lg:gap-x-1.5" + xl: "p-1 text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl md:pt-1 md:pb-0 md:pr-1 md:pl-1.5 lg:pl-4 sm:gap-x-3 md:gap-x-4 lg:gap-x-5" + + content_size_class: + + sm: "text-base" + md: "grid transition-grid-rows text-base md:text-lg" + lg: "md:text-lg lg:text-xl" + xl: "md:text-lg lg:text-xl xl:text-2xl" + """ + + def accordion(assigns) do + ~H""" + <.accordion_set> + <:header> + Title something 1 + + <:content> + Content something 1 + + + """ + end + + slot :header, required: true do + attr :button_id, :string + end + + slot :content, required: true + attr :header_size_class, :string, default: "text-base sm:text-lg md:text-xl" + attr :content_size_class, :string, default: "text-base md:text-lg" + attr :rest, :global + + def accordion_set(assigns) do + assigns = + assigns + |> assign(:header, List.wrap(assigns.header)) + |> assign(:content, List.wrap(assigns.content)) + |> assign(:random_id, :rand.uniform(9999)) + |> assign(:items, Enum.with_index(Enum.zip(List.wrap(assigns.header), List.wrap(assigns.content)))) + + ~H""" +
+
+ <% # Accordion Trigger %> + + + <% # Accordion Content %> + +
+
+ """ + end +end diff --git a/sources/lib/components/station_ui/html/avatars.ex b/sources/lib/components/station_ui/html/avatars.ex new file mode 100644 index 0000000..e2e366f --- /dev/null +++ b/sources/lib/components/station_ui/html/avatars.ex @@ -0,0 +1,239 @@ +defmodule StationUI.HTML.Avatars do + use Phoenix.Component + + import StationUI.HTML.StatusBadges, only: [status_badge: 1] + + @moduledoc """ + The avatar component renders initials, an SVG, or an image thumbnail to represent a user. + Avatars can be displayed as single items or combined into a horizontal stack. + + Sets up an avatar stack. + + ## Stack Example + + <.avatar_stack overflow_link={~p"/avatars/link"} display_max={2}> + <:avatar> + <.avatar .../> + + <:avatar> + <.avatar .../> + + <:avatar> + <.avatar .../> + + <:avatar> + <.avatar .../> + + <.avatar_stack> + + """ + @stack_base_classes [ + "flex items-start [&_div]:flex [&_div]:flex-row-reverse", + "[&>a]:z-20 [&>a]:hover:z-40 [&_div_a_figure]:z-10", + "[&_div_a]:hover:z-30 [&_a:focus-visible]:z-50 [&_a]:active:z-50 [&_a:hover_figure]:ml-0 [&_a:focus-visible_figure]:ml-0" + ] + + def stack_base_classes, do: @stack_base_classes + + attr(:class, :any, default: "[&_div]:ml-1.5 [&_div_figure]:-ml-3.5") + attr(:display_max, :integer, default: 3) + attr(:total_count, :integer, default: nil) + attr(:overflow_link, :string, required: true) + + slot(:avatar) + + def avatar_stack(assigns) do + assigns = + assigns + |> assign(:total_count, assigns.total_count || length(assigns.avatar)) + + ~H""" +
+ <.avatar_link :if={@total_count > @display_max} to={@overflow_link} variant="initials" class="h-[42px] w-[42px] border-[--sui-brand-primary-border]"> + <:initials count={true}>+<%= @total_count - @display_max %> + + +
+ <%= for {avatar, i} <- Enum.with_index(@avatar), i < @display_max do %> + <%= render_slot(avatar) %> + <% end %> +
+
+ """ + end + + @doc """ + An avatar that links somewhere. + + ## Example + + <.avatar_link to={~p"/some/link"} variant="placeholder" /> + + """ + @link_base_classes "rounded-full outline-none transition hover:ring-2 hover:ring-[--sui-brand-primary-muted] focus-visible:ring-[--sui-brand-primary-focus] focus-visible:ring-offset-4 active:ring-[--sui-brand-primary]" + + def link_base_classes, do: @link_base_classes + + attr(:status, :string, values: ~w[active inactive deactivated pending]) + attr(:variant, :string, values: ~w[image initials placeholder]) + attr(:index, :integer) + attr(:name, :string, default: nil) + attr(:image_src, :string, default: nil) + attr(:to, :string, required: true) + attr(:link_class, :any, default: "focus-visible:ring-2 active:ring-1") + attr(:class, :any, default: nil) + + # These are all passed through. + slot :initials do + attr(:count, :boolean) + end + + slot(:placeholder) + + def avatar_link(assigns) do + assigns = + case assigns do + %{class: nil} = assigns -> Map.drop(assigns, [:class]) + assigns -> assigns + end + + ~H""" + + <.avatar {Map.drop(assigns, [:link_class])} /> + + """ + end + + @doc """ + A single avatar + + ## Examples + + Avatar with initials, a border, and an active status icon: + + <.avatar variant="initials" status="active" class="h-[42px] w-[42px] border-[--sui-brand-primary]" /> + + Avatar with placeholder image with a pending status icon: + + <.avatar variant="placeholder" status="pending" /> + + Suggested classes for various sizes: + - xs -> "h-6 w-6 [&_svg]:w-3 text-xs" + - sm -> "h-8 w-8 [&_svg]:w-4 text-sm" + - md -> "h-[42px] w-[42px] [&_svg]:w-[21px]" (default) + - lg -> "h-[52px] w-[52px] [&_svg]:w-[26px] text-lg" + - xl -> "h-16 w-16 [&_svg]:w-8 text-lg" + + """ + @figure_base_classes "relative flex items-center justify-center border rounded-full bg-slate-50 transition-all duration-200 font-sans font-medium uppercase text-[--sui-brand-primary]" + + def figure_base_classes, do: @figure_base_classes + + attr(:status, :string, values: ~w[active inactive deactivated pending]) + attr(:variant, :string, values: ~w[image initials placeholder]) + attr(:index, :integer) + attr(:name, :string, default: nil) + attr(:image_src, :string, default: "") + attr(:class, :any, default: "h-[42px] w-[42px] [&_svg]:w-[21px] border-transparent") + + slot :initials do + attr(:count, :boolean) + end + + # We may have to deal with applying styles to placeholders? + slot(:placeholder) + + def avatar(%{variant: "image"} = assigns) do + ~H""" +
+ {@name + <.avatar_status_badge :if={assigns[:status]} status={@status} /> +
+ """ + end + + def avatar(%{variant: "initials"} = assigns) do + ~H""" +
+
+ + + <%= render_slot(@initials) %> + + <%= @name %> +
+ <.avatar_status_badge :if={assigns[:status]} status={@status} /> +
+ """ + end + + def avatar(%{variant: "placeholder"} = assigns) do + ~H""" +
+ <%= render_slot(@placeholder) || default_avatar_placeholder_icon(assigns) %> + <.avatar_status_badge :if={assigns[:status]} status={@status} /> +
+ """ + end + + defp initials_from_name(name) do + String.split(name) |> Enum.map_join(&String.first/1) + end + + @doc """ + The default placeholder icon for a placeholder variant of an avatar. + """ + attr(:name, :string, default: nil) + + def default_avatar_placeholder_icon(assigns) do + ~H""" + + <%= @name %> + + + + + + """ + end + + @doc """ + An avatar-specific status icon. + """ + attr(:status, :string, required: true, values: ~w[active inactive deactivated pending]) + attr(:class, :any, default: nil, doc: "additional or overriding classes") + + def avatar_status_badge(assigns) do + ~H""" + <.status_badge + :if={@status} + status={@status} + class={[ + "absolute -right-px -bottom-px z-10 transition-opacity duration-200", + "after:absolute after:inset-0", + "after:h-full after:w-full after:rounded-full", + "w-3 [&>span]:w-0.5" + ]} + /> + """ + end +end diff --git a/sources/lib/components/station_ui/html/banners.ex b/sources/lib/components/station_ui/html/banners.ex new file mode 100644 index 0000000..4192d63 --- /dev/null +++ b/sources/lib/components/station_ui/html/banners.ex @@ -0,0 +1,72 @@ +defmodule StationUI.HTML.Banners do + use Phoenix.Component + + import StationUI.HTML.Icons, only: [icon: 1] + import StationUI.HTML.Buttons + + alias Phoenix.LiveView.JS + + @base_classes "max-w-[800px] text-[--sui-brand-primary-text] w-full rounded-lg border py-2.5 pl-3" + defp base_classes, do: @base_classes + + @doc """ + The banner component renders an enclosed title, description, and close button. + The title content goes into the main inner_block slot. + The optional secondary (lower) content goes into the secondary slot. + + ## Examples + + Default banner with left icon, title, and secondary text: + + <.banner id="icon-title-and-secondary"> + <.icon name="hero-information-circle-solid" class="text-[--sui-brand-primary] shrink-0" /> +

Default Banner with Icon and Secondary

+ <:secondary> + Secondary text. + + + + Banner of default size but without border: + + <.banner id="no-border" class="border-transparent [&_span]:h-6 [&_span]:w-6 text-base"> + ... + + + Suggested classes for various text sizes and the default border styling: + + - xs -> "border-[--sui-brand-primary-border] [&_span]:h-3.5 [&_span]:w-3.5 text-xs" + - sm -> "border-[--sui-brand-primary-border] [&_span]:h-4.5 [&_span]:w-4.5 text-sm" + - md -> "border-[--sui-brand-primary-border] [&_span]:h-6 [&_span]:w-6 text-base" (the default) + - lg -> "border-[--sui-brand-primary-border] [&_span]:h-9 [&_span]:w-9 text-xl" + - xl -> "border-[--sui-brand-primary-border] [&_span]:h-12 [&_span]:w-12 text-3xl" + """ + + slot :inner_block, required: true + slot :secondary + + attr :id, :string, required: true + attr :class, :any, default: "border-[--sui-brand-primary-border] [&_span]:h-6 [&_span]:w-6 text-base" + attr :on_cancel, JS, default: %JS{} + + def banner(assigns) do + ~H""" +
+
+
+ <%= render_slot(@inner_block) %> +
+ <.button class="sui-secondary min-h-11 border-0 bg-white" aria-label="Dismiss" phx-click={hide_banner(@on_cancel, @id)}> + <.icon name="hero-x-mark" /> + +
+

<%= render_slot(@secondary) %>

+
+ """ + end + + defp hide_banner(js, id) do + js + |> JS.hide(to: "##{id}") + |> JS.pop_focus() + end +end diff --git a/sources/lib/components/station_ui/html/buttons.ex b/sources/lib/components/station_ui/html/buttons.ex new file mode 100644 index 0000000..44bd6f5 --- /dev/null +++ b/sources/lib/components/station_ui/html/buttons.ex @@ -0,0 +1,84 @@ +defmodule StationUI.HTML.Buttons do + use Phoenix.Component + + @moduledoc """ + The button component renders a + """ + end + + defp base_classes do + ~w" + [:where(&)]:rounded-lg + [:where(&)]:text-base + + py-[7px] + bg-[--sui-bg-btn] + border-[--sui-border-btn] + text-[--sui-text-btn] + inline-flex + items-center + justify-center + gap-x-1.5 + whitespace-nowrap + border + px-4 + font-bold + + hover:bg-[--sui-bg-btn-hover] + hover:border-[--sui-border-btn-hover] + hover:text-[--sui-text-btn-hover] + + focus-visible:outline-none + focus-visible:ring-2 + focus-visible:ring-purple-500 + focus-visible:ring-offset-4 + + active:bg-[--sui-bg-btn-active] + active:border-[--sui-border-btn-active] + active:text-[--sui-text-btn-active] + + disabled:bg-[--sui-bg-btn-disabled] + disabled:border-[--sui-border-btn-disabled] + disabled:text-[--sui-text-btn-disabled] + + lg:gap-x-2 + " + end +end diff --git a/sources/lib/components/station_ui/html/cards.ex b/sources/lib/components/station_ui/html/cards.ex new file mode 100644 index 0000000..273b511 --- /dev/null +++ b/sources/lib/components/station_ui/html/cards.ex @@ -0,0 +1,180 @@ +defmodule StationUI.HTML.Cards do + use Phoenix.Component + + @moduledoc """ + The cards component renders a self-contained area of content which can contain: + - Title + - Image + - Description + - Date + - Read More link + + The card can utilize either a vertical or horizontal layout. + + ## Examples + + ### Vertical Card + + <.card> + <:header> + A whale leaps out of the water + + <:content> +
+ +

+ The Whales Are Here! +

+

Nov 12, 2022

+
+

+ Lorem ipsum, dolor sit amet consectetur adipisicing elit. Facilis fugiat, aliquam assumenda repellat rerum nostrum. +

+ + + Read More + + + + + ### Horizontal Card + + <.card_horizontal> + <:header> + A whale leaps out of the water + + <:content> +
+

+ Headline +

+

+ Lorem ipsum, dolor sit amet consectetur adipisicing elit. Facilis fugiat, aliquam assumenda repellat rerum nostrum. +

+
+ +
+ <.button class="w-10 h-10 rounded-full border-0 sui-secondary"> + <.icon name="hero-face-smile-solid" class="w-5 h-5 shrink-0" /> + + <.button class="w-10 h-10 rounded-full border-0 sui-secondary"> + <.icon name="hero-face-smile-solid" class="w-5 h-5 shrink-0" /> + + <.button class="w-10 h-10 rounded-full border-0 sui-secondary"> + <.icon name="hero-face-smile-solid" class="w-5 h-5 shrink-0" /> + +
+ + + """ + + @base_classes "@container min-w-[200px] w-full h-full" + defp base_classes, do: @base_classes + + @base_inner_classes "overflow-hidden drop-shadow-md @[425px]:drop-shadow-lg @[625px]:drop-shadow-xl @[850px]:drop-shadow-2xl rounded-xl w-auto h-full" + defp base_inner_classes, do: @base_inner_classes + + @base_content_classes "grid gap-0.5 @[350px]:gap-1 @[425px]:gap-2 p-2 @[425px]:px-4 @[625px]:px-6 @[625px]:py-3 @[850px]:px-8 @[850px]:py-4" + defp base_content_classes, do: @base_content_classes + + attr :class, :any, default: "" + + slot :header + + slot :content, required: true do + attr :class, :string + end + + def card(assigns) do + ~H""" +
+
+
+ <%= render_slot(header) %> +
+ <.content_card slot={@content} /> +
+
+ """ + end + + attr :slot, :any, required: true + + defp content_card(assigns) do + class = + case assigns.slot do + [%{class: class} | _] -> class + _ -> "bg-white" + end + + assigns = assign(assigns, :class, class) + + ~H""" +
+ <%= render_slot(@slot) %> +
+ """ + end + + @base_horizontal_classes "@container min-w-[200px] w-full h-full" + defp base_horizontal_classes, do: @base_horizontal_classes + + @base_horizontal_inner_classes "overflow-hidden drop-shadow-md @[425px]:drop-shadow-lg @[625px]:drop-shadow-xl @[850px]:drop-shadow-2xl rounded-xl w-full flex" + defp base_horizontal_inner_classes, do: @base_horizontal_inner_classes + + @base_horizontal_content_classes "flex w-full gap-1 py-2 pl-2 @[425px]:py-4 @[425px]:pl-4 @[625px]:py-6 @[625px]:pl-6 @[850px]:py-8 @[850px]:pl-8" + defp base_horizontal_content_classes, do: @base_horizontal_content_classes + + attr :class, :any, default: "" + slot :inner_block, required: true + slot :header + + slot :content do + attr :class, :string + end + + def card_horizontal(assigns) do + ~H""" +
+
+
+ <%= render_slot(header) %> +
+ <.content_card_horizontal slot={@content} /> +
+
+ """ + end + + attr :slot, :any, required: true + + defp content_card_horizontal(assigns) do + class = + case assigns.slot do + [%{class: class} | _] -> class + _ -> "bg-white" + end + + assigns = assign(assigns, :class, class) + + ~H""" +
+ <%= render_slot(@slot) %> +
+ """ + end +end diff --git a/sources/lib/components/station_ui/html/footer.ex b/sources/lib/components/station_ui/html/footer.ex new file mode 100644 index 0000000..5d2d9e1 --- /dev/null +++ b/sources/lib/components/station_ui/html/footer.ex @@ -0,0 +1,224 @@ +defmodule StationUI.HTML.Footer do + use Phoenix.Component + + @moduledoc """ + The Footer component includes "simple" (default), and "columns" variant. + The default variant will list any footer_link slotted in horizontally, while + the columns variant will loop over a grouped list of links under a heading. + + ## Default Footer example + <.footer logo_src={~p"/images/my_logo.png"} logo_alt_text="[Organization name] logo"> + <:footer_link> + <.link patch={~p"/"}> + Home + + + <:footer_link> + <.link href="https://www.foo.com/blog"> + Blog + + + + + ## columns variant + <.footer variant="columns" logo_src={~p"/images/my_logo.png"}> + <:column heading="One"> + <.column_items> + <:footer_link> + <.link patch={~p"/"}> + Home + + + <:footer_link > + <.link href="https://www.foo.com/blog"> + Blog + + + + + <:column heading="Two"> + <.column_items> + <:footer_link> + <.link patch={~p"/"}> + Home + + + <:footer_link> + <.link href="https://www.foo.com/blog"> + Blog + + + + + <./footer> + + ## Both variants accept social links and icons + <.footer> + <:social_icon url="https://www.instagram.com" title="Instagram"> + + ... + + + <:social_icon url="https://www.facebook.com" title="Facebook" class="text-[#1877F2]"> + + ... + + + + """ + + slot :inner_block + + slot :footer_link do + attr :class, :string + end + + slot :column do + attr :heading, :string, required: true + end + + slot :social_icon do + attr :url, :string, required: true + attr :title, :string, required: true + attr :class, :string + end + + attr :variant, :string, default: "simple", values: ~w[simple columns] + attr :logo_src, :string, default: nil + attr :logo_alt_text, :string, default: "" + attr :legal_text, :string, default: "© #{DateTime.utc_now().year} Your Company, Inc. All rights reserved." + + def footer(%{variant: "simple"} = assigns) do + ~H""" +
+
+ <%= if @logo_src do %> +
+
+ {@logo_alt_text} +
+
+ <% end %> + <%= if !Enum.empty?(@footer_link) do %> +
    + <%= for footer_link <- @footer_link do %> + + <% end %> +
+ <% end %> +
+
+ <%= render_slot(@inner_block) %> +
+
+ + <%= @legal_text %> + +
+ <%= if !Enum.empty?(@social_icon) do %> +
    + <%= for social_icon <- @social_icon do %> +
  • + <.link href={social_icon[:url]} aria-label={social_icon[:title]} target="_blank"> + <%= render_slot(social_icon) %> + +
  • + <% end %> +
+ <% end %> +
+
+ """ + end + + def footer(%{variant: "columns"} = assigns) do + ~H""" +
+
+ <%= if @logo_src do %> +
+
+ {@logo_alt_text} +
+
+ <% end %> +
+ <%= for column <- @column do %> +
+
+ <%= column[:heading] %> +
+ <%= render_slot(column) %> +
+ <% end %> +
+
+
+ <%= render_slot(@inner_block) %> +
+
+ + <%= @legal_text %> + +
+ <%= if !Enum.empty?(@social_icon) do %> +
    + <%= for social_icon <- @social_icon do %> +
  • + <.link href={social_icon[:url]} aria-label={social_icon[:title]} target="_blank"> + <%= render_slot(social_icon) %> + +
  • + <% end %> +
+ <% end %> +
+
+ """ + end + + slot :footer_link do + attr :class, :string + end + + def column_items(assigns) do + ~H""" +
    + <%= for footer_link <- @footer_link do %> + + <% end %> +
+ """ + end + + defp footer_link_base_classes do + ~w" + font-bold + text-4xl + [&_a]:rounded-lg + [&_a:hover]:underline + [&_a:hover]:underline-offset-8 + [&_a:focus-visible]:outline-none + [&_a:focus-visible]:ring-4 + [&_a:focus-visible]:ring-purple-500 + [&_a:focus-visible]:ring-offset-4 + [&_a:focus-visible]:ring-offset-[--sui-brand-secondary-bg] + " + end + + defp social_icons_base_classes do + ~w" + [&_a]:block + [&_a]:rounded-lg + [&_a:focus-visible]:outline-none + [&_a:focus-visible]:ring-4 + [&_a:focus-visible]:ring-purple-500 + [&_a:focus-visible]:ring-offset-4 + [&_a:focus-visible]:ring-offset-[--sui-brand-secondary-bg] + " + end +end diff --git a/sources/lib/components/station_ui/html/forms.ex b/sources/lib/components/station_ui/html/forms.ex new file mode 100644 index 0000000..07e0e46 --- /dev/null +++ b/sources/lib/components/station_ui/html/forms.ex @@ -0,0 +1,100 @@ +defmodule StationUI.HTML.Forms do + @moduledoc """ + This module exists to provide the same API as the Phoenix Core Components so as to support + generators that target the Core Components (`mix phx.gen.live`, `mix phx.gen.auth`, etc...) + """ + use Phoenix.Component + alias StationUI.HTML.Inputs + + 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 + ~H""" + + <:label :if={@label}><%= @label %> + + """ + end + + def input(%{type: "select"} = assigns) do + ~H""" + + <:label :if={@label}><%= @label %> + + """ + end + + def input(%{type: "textarea"} = assigns) do + ~H""" + + <:label :if={@label}><%= @label %> + + """ + end + + def input(assigns) do + ~H""" + + <:label :if={@label}><%= @label %> + + """ + end + + @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(StationUI.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(StationUI.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/sources/lib/components/station_ui/html/icons.ex b/sources/lib/components/station_ui/html/icons.ex new file mode 100644 index 0000000..4c54de5 --- /dev/null +++ b/sources/lib/components/station_ui/html/icons.ex @@ -0,0 +1,28 @@ +defmodule StationUI.HTML.Icons do + use Phoenix.Component + + @doc """ + Renders a [Heroicon](https://heroicons.com). + + Heroicons come in three styles – outline, solid, and mini. + By default, the outline style is used, but solid and mini may + be applied by using the `-solid` and `-mini` suffix. + + You can customize the size and colors of the icons by setting + width, height, and background color classes. + + ## Examples + + <.icon name="hero-x-mark-solid" /> + <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> + """ + attr(:name, :string, required: true) + attr(:class, :any, default: nil) + attr(:rest, :global) + + def icon(%{name: "hero-" <> _} = assigns) do + ~H""" +