diff --git a/lib/mix/tasks/station_ui.install.ex b/lib/mix/tasks/station_ui.install.ex index 7673a7f..ce7c130 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]) @@ -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,31 +201,38 @@ 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) + 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") - ) + 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." + ) 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}") + Mix.shell().info("Replaced #{core_components_path}") + else + Mix.shell().info("Skipping replacing CoreComponents") + 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/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""" + + """ + end + + def footer(%{variant: "columns"} = assigns) do + ~H""" + + """ + end + + slot :footer_link do + attr :class, :string + end + + def column_items(assigns) do + ~H""" + + """ + 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""" +