From beeb4eccafd5b847ec16fab0f26753b8ad98a89b Mon Sep 17 00:00:00 2001
From: APB9785 <74077809+APB9785@users.noreply.github.com>
Date: Fri, 13 Dec 2024 18:25:19 -0600
Subject: [PATCH 1/2] Add StationUI
---
assets/css/beacon_live_admin.css | 2 +
assets/css/station-ui-fonts.css | 153 +++
assets/css/station-ui.css | 111 ++
assets/js/station-ui.js | 45 +
assets/tailwind.config.js | 3 +
.../live_admin/components/admin_components.ex | 244 +---
.../live_admin/components/station_ui/html.ex | 30 +
.../components/station_ui/html/accordion.ex | 142 +++
.../components/station_ui/html/avatar.ex | 239 ++++
.../components/station_ui/html/banner.ex | 72 ++
.../components/station_ui/html/button.ex | 84 ++
.../components/station_ui/html/card.ex | 180 +++
.../components/station_ui/html/footer.ex | 224 ++++
.../components/station_ui/html/form.ex | 100 ++
.../components/station_ui/html/icon.ex | 28 +
.../components/station_ui/html/input.ex | 1026 +++++++++++++++++
.../components/station_ui/html/modal.ex | 162 +++
.../components/station_ui/html/navbar.ex | 102 ++
.../station_ui/html/notification_badge.ex | 35 +
.../components/station_ui/html/pagination.ex | 224 ++++
.../components/station_ui/html/spinner.ex | 96 ++
.../station_ui/html/status_badge.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 +
.../components/station_ui/html/tag.ex | 74 ++
.../components/station_ui/html/toast.ex | 68 ++
.../components/station_ui/html/toolbar.ex | 128 ++
.../components/station_ui/html/tooltip.ex | 34 +
lib/beacon/live_admin/page_builder.ex | 1 +
lib/beacon/live_admin/web.ex | 1 +
31 files changed, 3643 insertions(+), 241 deletions(-)
create mode 100644 assets/css/station-ui-fonts.css
create mode 100644 assets/css/station-ui.css
create mode 100644 assets/js/station-ui.js
create mode 100644 lib/beacon/live_admin/components/station_ui/html.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/accordion.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/avatar.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/banner.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/button.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/card.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/footer.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/form.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/icon.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/input.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/modal.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/navbar.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/notification_badge.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/pagination.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/spinner.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/status_badge.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/tab_group.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/table_cell.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/table_header.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/tag.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/toast.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/toolbar.ex
create mode 100644 lib/beacon/live_admin/components/station_ui/html/tooltip.ex
diff --git a/assets/css/beacon_live_admin.css b/assets/css/beacon_live_admin.css
index 11232c7a..20d37c1a 100644
--- a/assets/css/beacon_live_admin.css
+++ b/assets/css/beacon_live_admin.css
@@ -1,3 +1,5 @@
+@import "./station-ui.css";
+@import "./station-ui-fonts.css";
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
diff --git a/assets/css/station-ui-fonts.css b/assets/css/station-ui-fonts.css
new file mode 100644
index 00000000..9c99519e
--- /dev/null
+++ b/assets/css/station-ui-fonts.css
@@ -0,0 +1,153 @@
+/* Inter-100 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 100;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-Thin.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-Thin.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-Thin.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-200 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 200;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-ExtraLight.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-ExtraLight.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-ExtraLight.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-300 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 300;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-Light.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-Light.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-Light.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-400 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-Regular.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-Regular.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-Regular.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-500 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-Medium.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-Medium.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-Medium.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-600 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 600;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-SemiBold.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-SemiBold.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-SemiBold.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-700 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-Bold.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-Bold.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-Bold.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-800 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 800;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-ExtraBold.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-ExtraBold.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-ExtraBold.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* Inter-900 */
+@font-face {
+ font-family: "Inter";
+ font-weight: 900;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/inter/Inter-Black.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/inter/Inter-Black.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/inter/Inter-Black.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* roboto-mono-300 - latin_latin-ext */
+@font-face {
+ font-family: "Roboto Mono";
+ font-weight: 300;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/roboto-mono/roboto-mono-v23-latin_latin-ext-300.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/roboto-mono/roboto-mono-v23-latin_latin-ext-300.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/roboto-mono/roboto-mono-v23-latin_latin-ext-300.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
+
+/* roboto-mono-500 - latin_latin-ext */
+@font-face {
+ font-family: "Roboto Mono";
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+ src: url("/fonts/roboto-mono/roboto-mono-v23-latin_latin-ext-500.woff2"),
+ /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
+ url("/fonts/roboto-mono/roboto-mono-v23-latin_latin-ext-500.woff"),
+ /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+, iOS 5+ */
+ url("/fonts/roboto-mono/roboto-mono-v23-latin_latin-ext-500.ttf");
+ /* Chrome 4+, Firefox 3.5+, IE 9+, Safari 3.1+, iOS 4.2+, Android Browser 2.2+ */
+}
diff --git a/assets/css/station-ui.css b/assets/css/station-ui.css
new file mode 100644
index 00000000..5182614c
--- /dev/null
+++ b/assets/css/station-ui.css
@@ -0,0 +1,111 @@
+@layer base {
+ :root {
+ /* primary */
+ --sui-brand-primary: theme("colors.indigo.700");
+ --sui-brand-primary-bg: theme("colors.indigo.600");
+ --sui-brand-primary-bg-disabled: theme("colors.slate.50");
+ --sui-brand-primary-muted: theme("colors.indigo.500");
+ --sui-brand-primary-shadow: theme("colors.slate.100");
+ --sui-brand-primary-text: theme("colors.slate.800");
+ --sui-brand-primary-text-inverted: theme("colors.white");
+ --sui-brand-primary-text-disabled: theme("colors.slate.300");
+ --sui-brand-primary-border: theme("colors.slate.300");
+ --sui-brand-primary-border-inverted: theme("colors.slate.600");
+ --sui-brand-primary-focus: theme("colors.purple.500");
+ --sui-brand-primary-error: theme("colors.rose.500");
+ --sui-brand-primary-success: theme("colors.emerald.500");
+ --sui-brand-primary-icon: theme("colors.slate.500");
+ --sui-brand-primary-icon-inverted: theme("colors.slate.400");
+
+ /* secondary */
+ --sui-brand-secondary-bg: theme("colors.white");
+ --sui-brand-secondary-bg-inverted: theme("colors.black");
+ --sui-brand-secondary-text: theme("colors.slate.600");
+ --sui-brand-secondary-text-muted: theme("colors.gray.500");
+ --sui-brand-secondary-text-inverted: theme("colors.slate.400");
+
+ /* buttons */
+ --sui-text-btn-disabled: theme("colors.slate.300");
+ --sui-bg-btn-disabled: theme("colors.slate.50");
+ --sui-border-btn-disabled: theme("colors.slate.50");
+
+ /* forms */
+ --sui-form-bg-slider-progress: theme("colors.indigo.600");
+ --sui-form-bg-slider-progress-disabled: theme("colors.zinc.300");
+ --sui-form-bg-slider-thumb: theme("colors.indigo.600");
+ --sui-form-bg-slider-thumb-active: theme("colors.indigo.800");
+ --sui-form-bg-slider-thumb-disabled: theme("colors.slate.100");
+ --sui-form-bg-slider-thumb-hover: theme("colors.indigo.500");
+ --sui-form-bg-slider-track: theme("colors.white");
+ --sui-form-bg-slider-track-disabled: theme("colors.slate.50");
+ --sui-form-border-slider-thumb: theme("colors.indigo.500");
+ --sui-form-border-slider-thumb-active: theme("colors.indigo.700");
+ --sui-form-border-slider-thumb-disabled: theme("colors.zinc.300");
+ --sui-form-border-slider-thumb-hover: theme("colors.indigo.400");
+ --sui-form-border-slider-track: theme("colors.gray.400");
+ --sui-form-border-slider-track-disabled: theme("colors.zinc.300");
+ --sui-form-text: var(--sui-brand-primary-text);
+ --sui-form-text-disabled: theme("colors.gray.500");
+ --sui-form-text-error: theme("colors.rose.700");
+ }
+
+ .sui-primary {
+ --sui-text-btn: theme("colors.white");
+ --sui-text-btn-hover: theme("colors.white");
+ --sui-text-btn-active: theme("colors.white");
+ --sui-bg-btn: theme("colors.indigo.700");
+ --sui-bg-btn-hover: theme("colors.indigo.600");
+ --sui-bg-btn-active: theme("colors.indigo.800");
+ --sui-border-btn: theme("colors.indigo.700");
+ --sui-border-btn-hover: theme("colors.indigo.600");
+ --sui-border-btn-active: theme("colors.indigo.800");
+ }
+
+ .sui-secondary {
+ --sui-text-btn: theme("colors.slate.800");
+ --sui-text-btn-hover: theme("colors.slate.800");
+ --sui-text-btn-active: theme("colors.slate.800");
+ --sui-bg-btn: theme("colors.white");
+ --sui-bg-btn-hover: theme("colors.slate.50");
+ --sui-bg-btn-active: theme("colors.slate.200");
+ --sui-border-btn: theme("colors.slate.800");
+ --sui-border-btn-hover: theme("colors.slate.800");
+ --sui-border-btn-active: theme("colors.slate.800");
+ }
+
+ .sui-tertiary {
+ --sui-text-btn: theme("colors.white");
+ --sui-text-btn-hover: theme("colors.white");
+ --sui-text-btn-active: theme("colors.white");
+ --sui-bg-btn: theme("colors.slate.800");
+ --sui-bg-btn-hover: theme("colors.slate.700");
+ --sui-bg-btn-active: theme("colors.slate.900");
+ --sui-border-btn: theme("colors.slate.800");
+ --sui-border-btn-hover: theme("colors.slate.700");
+ --sui-border-btn-active: theme("colors.slate.900");
+ }
+
+ .sui-primary-destructive {
+ --sui-text-btn: theme("colors.white");
+ --sui-text-btn-hover: theme("colors.white");
+ --sui-text-btn-active: theme("colors.white");
+ --sui-bg-btn: theme("colors.rose.700");
+ --sui-bg-btn-hover: theme("colors.rose.600");
+ --sui-bg-btn-active: theme("colors.rose.800");
+ --sui-border-btn: theme("colors.rose.700");
+ --sui-border-btn-hover: theme("colors.rose.600");
+ --sui-border-btn-active: theme("colors.rose.800");
+ }
+
+ .sui-secondary-destructive {
+ --sui-text-btn: theme("colors.rose.700");
+ --sui-text-btn-hover: theme("colors.rose.600");
+ --sui-text-btn-active: theme("colors.rose.800");
+ --sui-bg-btn: theme("colors.white");
+ --sui-bg-btn-hover: theme("colors.white");
+ --sui-bg-btn-active: theme("colors.white");
+ --sui-border-btn: theme("colors.rose.700");
+ --sui-border-btn-hover: theme("colors.rose.600");
+ --sui-border-btn-active: theme("colors.rose.800");
+ }
+}
diff --git a/assets/js/station-ui.js b/assets/js/station-ui.js
new file mode 100644
index 00000000..1b6a7954
--- /dev/null
+++ b/assets/js/station-ui.js
@@ -0,0 +1,45 @@
+// Tailwind CSS presets for Station UI
+const defaultTheme = require("tailwindcss/defaultTheme");
+
+module.exports = {
+ theme: {
+ container: {
+ center: true,
+ },
+ extend: {
+ animation: {
+ "spin-reverse": "spin-reverse 1s linear infinite",
+ },
+ fontFamily: {
+ sans: ["Inter", ...defaultTheme.fontFamily.sans],
+ mono: ["Roboto Mono", ...defaultTheme.fontFamily.mono],
+ },
+ boxShadow: {
+ "inner-sm": "inset 0 1px 2px 0 rgb(0 0 0 / 0.05)", // shadow-sm
+ "inner-md":
+ "inset 0 4px 6px -1px rgb(0 0 0 / 0.1), inset 0 2px 4px -2px rgb(0 0 0 / 0.1)", // shadow-md
+ "inner-lg":
+ "inset 0 10px 15px -3px rgb(0 0 0 / 0.1), inset 0 4px 6px -4px rgb(0 0 0 / 0.1)", // shadow-lg
+ "inner-xl":
+ "inset 0 20px 25px -5px rgb(0 0 0 / 0.1), inset 0 8px 10px -6px rgb(0 0 0 / 0.1)", // shadow-xl
+ "inner-2xl": "inset 0 25px 50px -12px rgb(0 0 0 / 0.25)", // shadow-2xl
+ },
+ keyframes: {
+ "spin-reverse": {
+ from: {
+ transform: "rotate(360deg)",
+ },
+ },
+ },
+ spacing: {
+ 4.5: "1.125rem", //18px
+ },
+ transitionProperty: {
+ "grid-rows": "grid-template-rows",
+ },
+ }
+ },
+ plugins: [
+ require("@tailwindcss/container-queries"),
+ ]
+}
diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js
index 31e97108..7dcece6e 100644
--- a/assets/tailwind.config.js
+++ b/assets/tailwind.config.js
@@ -12,6 +12,9 @@ const fs = require("fs")
const path = require("path")
module.exports = {
+ presets: [
+ require('./js/station-ui.js'),
+ ],
content: ["./js/**/*.js", "../lib/beacon/live_admin/**/*.*ex", "./svelte/**/*.svelte"],
theme: {
extend: {
diff --git a/lib/beacon/live_admin/components/admin_components.ex b/lib/beacon/live_admin/components/admin_components.ex
index ecdd2983..ea59b7f6 100644
--- a/lib/beacon/live_admin/components/admin_components.ex
+++ b/lib/beacon/live_admin/components/admin_components.ex
@@ -14,16 +14,16 @@ defmodule Beacon.LiveAdmin.AdminComponents do
import Beacon.LiveAdmin.Router, only: [beacon_live_admin_path: 3]
defdelegate header(assigns), to: CoreComponents
- defdelegate icon(assigns), to: CoreComponents
- defdelegate show_modal(assigns), to: CoreComponents
defdelegate hide_modal(assigns), to: CoreComponents
defdelegate show(selector), to: CoreComponents
defdelegate show(js, selector), to: CoreComponents
defdelegate hide(selector), to: CoreComponents
defdelegate hide(js, selector), to: CoreComponents
- defdelegate translate_error(error), to: CoreComponents
defdelegate translate_errors(errors, field), to: CoreComponents
+ defp icon(assigns), do: Beacon.LiveAdmin.StationUI.HTML.Icon.icon(assigns)
+ defp input(assigns), do: Beacon.LiveAdmin.CoreComponents.input(assigns)
+
@menu_link_active_class "inline-block p-4 text-blue-600 border-b-2 border-blue-600 rounded-t-lg active"
@menu_link_regular_class "inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300"
@@ -211,58 +211,6 @@ defmodule Beacon.LiveAdmin.AdminComponents do
"""
end
- @doc """
- Renders a modal.
-
- ## Examples
-
- <.modal id="confirm-modal">
- This is a modal.
-
-
- JS commands may be passed to the `:on_cancel` to configure
- the closing/cancel event, for example:
-
- <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
- This is another modal.
-
-
- """
- attr :id, :string, required: true
- attr :show, :boolean, default: false
- attr :on_cancel, JS, default: %JS{}
- slot :inner_block, required: true
-
- def modal(assigns) do
- ~H"""
-
-
-
-
-
- <.focus_wrap
- id={"#{@id}-container"}
- phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
- phx-key="escape"
- phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
- class="relative hidden transition bg-white shadow-lg shadow-zinc-700/10 ring-zinc-700/10 rounded-2xl p-14 ring-1"
- >
-
-
-
-
- <%= render_slot(@inner_block) %>
-
-
-
-
-
-
- """
- end
-
@doc """
Renders flash notices.
@@ -361,192 +309,6 @@ defmodule Beacon.LiveAdmin.AdminComponents do
"""
end
- @doc """
- Renders a button.
-
- ## Examples
-
- <.button>Send!
- <.button phx-click="go" class="ml-2">Send!
- """
- attr :type, :string, default: nil
- attr :class, :string, default: nil
- attr :rest, :global, include: ~w(disabled form name value)
-
- slot :inner_block, required: true
-
- def button(assigns) do
- ~H"""
-
- """
- end
-
- @doc """
- Renders an input with label and error messages.
-
- A `%Phoenix.HTML.Form{}` and field name may be passed to the input
- to build input names and error messages, or all the attributes and
- errors may be passed explicitly.
-
- ## 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(autocomplete cols disabled form list max maxlength min minlength
- 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", value: value} = assigns) do
- assigns =
- assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
-
- ~H"""
-
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- def input(%{type: "select"} = assigns) do
- ~H"""
-
- <%= if @label do %>
- <.label for={@id}><%= @label %>
- <% end %>
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- def input(%{type: "textarea"} = assigns) do
- ~H"""
-
- <%= if @label do %>
- <.label for={@id}><%= @label %>
- <% end %>
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- # All other inputs text, datetime-local, url, password, etc. are handled here...
- def input(assigns) do
- ~H"""
-
- <%= if @label do %>
- <.label for={@id}><%= @label %>
- <% end %>
-
- <.error :for={msg <- @errors}><%= msg %>
-
- """
- end
-
- @doc """
- Renders a label.
- """
- attr :for, :string, default: nil
- slot :inner_block, required: true
-
- def label(assigns) do
- ~H"""
-
- """
- 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 ~S"""
Renders a table with generic styling.
diff --git a/lib/beacon/live_admin/components/station_ui/html.ex b/lib/beacon/live_admin/components/station_ui/html.ex
new file mode 100644
index 00000000..4652e38d
--- /dev/null
+++ b/lib/beacon/live_admin/components/station_ui/html.ex
@@ -0,0 +1,30 @@
+defmodule Beacon.LiveAdmin.StationUI.HTML do
+ defmacro __using__(_) do
+ quote do
+ import Beacon.LiveAdmin.StationUI.HTML.{
+ Avatar,
+ Banner,
+ Button,
+ Accordion,
+ Card,
+ Footer,
+ Form,
+ Icon,
+ NotificationBadge,
+ Input,
+ Modal,
+ Navbar,
+ Pagination,
+ Spinner,
+ StatusBadge,
+ TabGroup,
+ Tag,
+ Toast,
+ Toolbar,
+ Tooltip,
+ TableHeader,
+ TableCell
+ }
+ end
+ end
+end
diff --git a/lib/beacon/live_admin/components/station_ui/html/accordion.ex b/lib/beacon/live_admin/components/station_ui/html/accordion.ex
new file mode 100644
index 00000000..1ca8265e
--- /dev/null
+++ b/lib/beacon/live_admin/components/station_ui/html/accordion.ex
@@ -0,0 +1,142 @@
+defmodule Beacon.LiveAdmin.StationUI.HTML.Accordion do
+ use Phoenix.Component
+
+ import Beacon.LiveAdmin.StationUI.HTML.Icon, only: [icon: 1]
+ alias Phoenix.LiveView.JS
+
+ @header_default_classes "text-base sm:text-lg md:text-xl"
+ defp header_default_classes, do: @header_default_classes
+
+ @content_default_classes "text-base md:text-lg"
+ defp content_default_classes, do: @content_default_classes
+
+ @doc """
+ The accordion component renders a list of items with child content that can be expanded or collapsed.
+
+ ## Example
+
+ <.accordion>
+ <:header>
+ Title something 1
+
+ <:content>
+ Content something 1
+
+
+
+ Suggested size classes for `header` (default: md):
+
+ 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"
+
+ Suggested size classes for `content` (default: md):
+
+ 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"
+ """
+
+ attr :id, :string
+ attr :rest, :global
+
+ slot :header, required: true do
+ attr :button_id, :string
+ attr :class, :string, doc: @header_default_classes
+ end
+
+ slot :content, required: true do
+ attr :class, :string, doc: @content_default_classes
+ end
+
+ def accordion(assigns) do
+ assigns =
+ assigns
+ |> assign_new(:id, fn -> :rand.uniform(9999) end)
+ |> assign(:items, assigns.header |> Enum.zip(assigns.content) |> Enum.with_index())
+
+ ~H"""
+
+
+ <% # Accordion Trigger %>
+
+
+ <% # Accordion Content %>
+
+
+ <%= render_slot(content) %>
+
+
+
+
+ """
+ end
+end
diff --git a/lib/beacon/live_admin/components/station_ui/html/avatar.ex b/lib/beacon/live_admin/components/station_ui/html/avatar.ex
new file mode 100644
index 00000000..ab29b575
--- /dev/null
+++ b/lib/beacon/live_admin/components/station_ui/html/avatar.ex
@@ -0,0 +1,239 @@
+defmodule Beacon.LiveAdmin.StationUI.HTML.Avatar do
+ use Phoenix.Component
+
+ import Beacon.LiveAdmin.StationUI.HTML.StatusBadge, 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"""
+
+ """
+ end
+
+ def avatar(%{variant: "initials"} = assigns) do
+ ~H"""
+
+ """
+ end
+
+ def avatar(%{variant: "placeholder"} = assigns) do
+ ~H"""
+
+ """
+ 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"""
+
+ """
+ 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/lib/beacon/live_admin/components/station_ui/html/banner.ex b/lib/beacon/live_admin/components/station_ui/html/banner.ex
new file mode 100644
index 00000000..3db27d54
--- /dev/null
+++ b/lib/beacon/live_admin/components/station_ui/html/banner.ex
@@ -0,0 +1,72 @@
+defmodule Beacon.LiveAdmin.StationUI.HTML.Banner do
+ use Phoenix.Component
+
+ import Beacon.LiveAdmin.StationUI.HTML.Icon, only: [icon: 1]
+ import Beacon.LiveAdmin.StationUI.HTML.Button
+
+ 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/lib/beacon/live_admin/components/station_ui/html/button.ex b/lib/beacon/live_admin/components/station_ui/html/button.ex
new file mode 100644
index 00000000..00272aee
--- /dev/null
+++ b/lib/beacon/live_admin/components/station_ui/html/button.ex
@@ -0,0 +1,84 @@
+defmodule Beacon.LiveAdmin.StationUI.HTML.Button do
+ use Phoenix.Component
+
+ @moduledoc """
+ The button component renders a