diff --git a/lib/salad_ui.ex b/lib/salad_ui.ex
index 9076900..1218c16 100644
--- a/lib/salad_ui.ex
+++ b/lib/salad_ui.ex
@@ -8,8 +8,9 @@ defmodule SaladUI do
# alias OrangeCmsWeb.Components.LadUI.LadJS
alias Phoenix.LiveView.JS
+
defp classes(input) do
- SaladUI.Merge.merge(input)
+ TwMerge.merge(input)
end
end
end
@@ -23,33 +24,43 @@ defmodule SaladUI do
defmacro __using__(_) do
quote do
+ import SaladUI.Accordion
import SaladUI.Alert
+ import SaladUI.AlertDialog
import SaladUI.Avatar
import SaladUI.Badge
import SaladUI.Breadcrumb
import SaladUI.Button
import SaladUI.Card
+ import SaladUI.Chart
import SaladUI.Checkbox
+ import SaladUI.Collapsible
import SaladUI.Dialog
import SaladUI.DropdownMenu
import SaladUI.Form
+ import SaladUI.Helpers
import SaladUI.HoverCard
import SaladUI.Icon
import SaladUI.Input
import SaladUI.Label
import SaladUI.Menu
import SaladUI.Pagination
+ import SaladUI.Popover
import SaladUI.Progress
+ import SaladUI.RadioGroup
import SaladUI.ScrollArea
import SaladUI.Select
import SaladUI.Separator
import SaladUI.Sheet
+ import SaladUI.Sidebar
import SaladUI.Skeleton
import SaladUI.Slider
import SaladUI.Switch
import SaladUI.Table
import SaladUI.Tabs
import SaladUI.Textarea
+ import SaladUI.Toggle
+ import SaladUI.ToggleGroup
import SaladUI.Tooltip
end
end
diff --git a/lib/salad_ui/alert.ex b/lib/salad_ui/alert.ex
index 8356497..690a8b3 100644
--- a/lib/salad_ui/alert.ex
+++ b/lib/salad_ui/alert.ex
@@ -86,7 +86,8 @@ defmodule SaladUI.Alert do
@variants %{
variant: %{
"default" => "bg-background text-foreground",
- "destructive" => "bg-background border-destructive/50 text-destructive dark:border-destructive [&>span]:text-destructive"
+ "destructive" =>
+ "bg-background border-destructive/50 text-destructive dark:border-destructive [&>span]:text-destructive"
}
}
diff --git a/lib/salad_ui/collapsible.ex b/lib/salad_ui/collapsible.ex
index ec6380f..3169496 100644
--- a/lib/salad_ui/collapsible.ex
+++ b/lib/salad_ui/collapsible.ex
@@ -22,24 +22,25 @@ defmodule SaladUI.Collapsible do
required: true,
doc: "Id to identify collapsible component, collapsible_trigger uses this id to toggle content visibility"
- attr :open, :boolean, default: false, doc: "Initial state of collapsible content"
+ attr :open, :boolean, default: true, doc: "Initial state of collapsible content"
attr :class, :string, default: nil
+ attr :rest, :global, include: ~w(title)
slot(:inner_block, required: true)
def collapsible(assigns) do
assigns =
- assigns
- |> assign(:builder, %{open: assigns[:open], id: assigns[:id]})
- |> assign(:open, normalize_boolean(assigns[:open]))
+ assign(assigns, :open, normalize_boolean(assigns[:open]))
~H"""
- <%= render_slot(@inner_block, @builder) %>
+ <%= render_slot(@inner_block) %>
"""
end
@@ -47,15 +48,22 @@ defmodule SaladUI.Collapsible do
@doc """
Render trigger for collapsible component.
"""
- attr :builder, :map, required: true, doc: "Builder instance for collapsible component"
attr(:class, :string, default: nil)
+ attr :as_tag, :any, default: "div"
+ attr :rest, :global
slot(:inner_block, required: true)
+
def collapsible_trigger(assigns) do
~H"""
- @builder.id)} class={@class}>
+ <.dynamic
+ tag={@as_tag}
+ onclick={exec_closest("phx-toggle-collapsible", ".collapsible-root")}
+ class={@class}
+ {@rest}
+ >
<%= render_slot(@inner_block) %>
-
+
"""
end
@@ -85,12 +93,14 @@ defmodule SaladUI.Collapsible do
@doc """
Show collapsible content.
"""
- def toggle_collapsible(js \\ %JS{}, %{id: id} = _builder) do
- JS.toggle(js,
+ def toggle_collapsible(js \\ %JS{}, id) do
+ js
+ |> JS.toggle(
to: "##{id} .collapsible-content",
in: {"ease-out duration-200", "opacity-0", "opacity-100"},
out: {"ease-out", "opacity-100", "opacity-70"},
time: 200
)
+ |> JS.toggle_attribute({"data-state", "open", "closed"}, to: "##{id}")
end
end
diff --git a/lib/salad_ui/dropdown_menu.ex b/lib/salad_ui/dropdown_menu.ex
index 7e14efe..addf10d 100644
--- a/lib/salad_ui/dropdown_menu.ex
+++ b/lib/salad_ui/dropdown_menu.ex
@@ -50,12 +50,15 @@ defmodule SaladUI.DropdownMenu do
end
attr :class, :string, default: nil
+ attr :as_tag, :any, default: "div"
slot :inner_block, required: true
+
attr :rest, :global
def dropdown_menu_trigger(assigns) do
~H"""
-
+
"""
end
@@ -94,6 +97,29 @@ defmodule SaladUI.DropdownMenu do
"""
end
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def dropdown_menu_shortcut(assigns) do
+ ~H"""
+
<.sheet_overlay />
<.focus_wrap
- id={"sheet-" <> @id}
- phx-window-keydown={JS.exec("phx-hide-sheet", to: "#" <> @id)}
+ id={"sheet-#{@id}"}
+ phx-window-keydown={@id && JS.exec("phx-hide-sheet", to: "#" <> @id)}
phx-key="escape"
- phx-click-away={JS.exec("phx-hide-sheet", to: "#" <> @id)}
+ phx-click-away={@id && JS.exec("phx-hide-sheet", to: "#" <> @id)}
role="sheet"
class={
classes([
diff --git a/lib/salad_ui/sidebar.ex b/lib/salad_ui/sidebar.ex
new file mode 100644
index 0000000..712ba22
--- /dev/null
+++ b/lib/salad_ui/sidebar.ex
@@ -0,0 +1,750 @@
+defmodule SaladUI.Sidebar do
+ @moduledoc false
+ use SaladUI, :component
+
+ import SaladUI.Input
+ import SaladUI.Separator
+ import SaladUI.Sheet
+ import SaladUI.Skeleton
+ import SaladUI.Tooltip
+
+ @sidebar_width "16rem"
+ @sidebar_width_mobile "18rem"
+ @sidebar_width_icon "3rem"
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr :style, :map, default: %{}
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_provider(assigns) do
+ assigns = assign(assigns, %{sidebar_width: @sidebar_width, sidebar_width_icon: @sidebar_width_icon})
+
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+
+ attr :id, :string, required: true, doc: "The id of the sidebar, used for the trigger to identify the target sidebar"
+ attr :side, :string, values: ~w(left right), default: "left"
+ attr :variant, :string, values: ~w(sidebar floating inset), default: "sidebar"
+ attr :collapsible, :string, values: ~w(offcanvas icon none), default: "offcanvas"
+ attr :is_mobile, :boolean, default: false
+ attr :state, :string, values: ~w(expanded collapsed), default: "expanded"
+ attr(:class, :string, default: nil)
+ attr :style, :map, default: %{}
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar(%{collapsible: "none"} = assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ def sidebar(%{is_mobile: true} = assigns) do
+ assigns = assign(assigns, :sidebar_width_mobile, @sidebar_width_mobile)
+
+ ~H"""
+ <.sheet>
+ <.sheet_content
+ data-sidebar="sidebar"
+ data-mobile="true"
+ class="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
+ style={
+ style([
+ %{
+ "--sidebar-width": @sidebar_width_mobile
+ },
+ @style
+ ])
+ }
+ side={@side}
+ >
+
+ <%= render_slot(@inner_block) %>
+
+
+
+ """
+ end
+
+ def sidebar(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr :target, :string, required: true, doc: "The id of the target sidebar"
+ attr :as_tag, :any, default: "button"
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_trigger(assigns) do
+ ~H"""
+ <.dynamic
+ tag={@as_tag}
+ data-sidebar="trigger"
+ variant="ghost"
+ size="icon"
+ class={classes(["h-7 w-7", @class])}
+ phx-click={JS.exec("phx-toggle-sidebar", to: "#" <> @target)}
+ {@rest}
+ >
+ <%= render_slot(@inner_block) %>
+
Toggle Sidebar
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+
+ def sidebar_rail(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot :inner_block, required: true
+
+ def sidebar_inset(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+
+ def sidebar_input(assigns) do
+ ~H"""
+ <.input
+ data-sidebar="input"
+ class={
+ classes([
+ "h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
+ @class
+ ])
+ }
+ {@rest}
+ />
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_header(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot :inner_block, required: true
+
+ def sidebar_footer(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+
+ def sidebar_separator(assigns) do
+ ~H"""
+ <.separator
+ data-sidebar="separator"
+ class={classes(["mx-2 w-auto bg-sidebar-border", @class])}
+ {@rest}
+ />
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_content(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_group(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ TODO: class merge not work well here
+ """
+ attr(:class, :string, default: nil)
+ attr :as_tag, :any, default: "div"
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_group_label(assigns) do
+ ~H"""
+ <.dynamic
+ data-sidebar="group-label"
+ tag={@as_tag}
+ class={
+ Enum.join(
+ [
+ "duration-200 flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0 text-xs",
+ "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
+ @class
+ ],
+ " "
+ )
+ }
+ {@rest}
+ >
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_group_action(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_group_content(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_menu(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_menu_item(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr :variant, :string, values: ~w(default outline), default: "default"
+ attr :size, :string, values: ~w(default sm lg), default: "default"
+ attr :is_active, :boolean, default: false
+ attr(:class, :string, default: nil)
+ attr :is_mobile, :boolean, default: false
+ attr :state, :string, default: "expanded"
+ attr :as_tag, :any, default: "button"
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+ attr :tooltip, :string, required: false
+
+ def sidebar_menu_button(assigns) do
+ button = ~H"""
+ <.dynamic
+ tag={@as_tag}
+ data-sidebar="menu-button"
+ data-size={@size}
+ data-active={@is_active}
+ class={classes([get_variant(%{variant: @variant, size: @size}), @class])}
+ {@rest}
+ >
+ <%= render_slot(@inner_block) %>
+
+ """
+
+ assigns = assign(assigns, :button, button)
+
+ if assigns[:tooltip] do
+ ~H"""
+ <.tooltip class="block">
+ <.tooltip_trigger>
+ <%= @button %>
+
+ <.tooltip_content side="right" hidden={@state != "collapsed" || @is_mobile}>
+ <%= @tooltip %>
+
+
+ """
+ else
+ button
+ end
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr :show_on_hover, :boolean, default: false
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_menu_action(assigns) do
+ ~H"""
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_menu_badge(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr :show_icon, :boolean, default: false
+ attr(:rest, :global)
+
+ def sidebar_menu_skeleton(assigns) do
+ width = :rand.uniform(40) + 50
+ assigns = assign(assigns, :width, width)
+
+ ~H"""
+
+ <.skeleton :if={@show_icon} class="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
+ <.skeleton
+ class="h-4 flex-1 max-w-[--skeleton-width]"
+ data-sidebar="menu-skeleton-text"
+ style={
+ style([
+ %{
+ "--skeleton-width": @width
+ }
+ ])
+ }
+ />
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_menu_sub(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr(:class, :string, default: nil)
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_menu_sub_item(assigns) do
+ ~H"""
+
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @doc """
+ Render
+ """
+ attr :size, :string, values: ~w(sm md), default: "md"
+ attr :is_active, :boolean, default: false
+ attr(:class, :string, default: nil)
+ attr :as_tag, :any, default: "a"
+ attr(:rest, :global)
+ slot(:inner_block, required: true)
+
+ def sidebar_menu_sub_button(assigns) do
+ ~H"""
+ <.dynamic
+ tag={@as_tag}
+ data-sidebar="menu-sub-button"
+ data-size={@size}
+ data-active={@is_active}
+ class={
+ classes([
+ "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
+ "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
+ @size == "sm" && "text-xs",
+ @size == "md" && "text-sm",
+ "group-data-[collapsible=icon]:hidden",
+ @class
+ ])
+ }
+ {@rest}
+ >
+ <%= render_slot(@inner_block) %>
+
+ """
+ end
+
+ @variant_config %{
+ variants: %{
+ variant: %{
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
+ outline:
+ "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]"
+ },
+ size: %{
+ default: "h-8 text-sm",
+ sm: "h-7 text-xs",
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0"
+ }
+ },
+ default_variants: %{
+ variant: "default",
+ size: "default"
+ }
+ }
+ @shared_classes "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0"
+ defp get_variant(input) do
+ @shared_classes <> " " <> variant_class(@variant_config, input)
+ end
+
+ @doc """
+ Toggle sidebar between collapsed and expanded state.
+ """
+ def toggle_sidebar({state1, state2} = _collapsible_states) do
+ {"data-state", "collapsed", "expanded"}
+ |> JS.toggle_attribute()
+ |> JS.toggle_attribute({"data-collapsible", state1, state2})
+ end
+end
diff --git a/lib/salad_ui/tooltip.ex b/lib/salad_ui/tooltip.ex
index 0bf91a2..ba729ed 100644
--- a/lib/salad_ui/tooltip.ex
+++ b/lib/salad_ui/tooltip.ex
@@ -63,7 +63,7 @@ defmodule SaladUI.Tooltip do
data-side={@side}
class={
classes([
- "tooltip-content absolute whitespace-nowrap hidden group-hover/tooltip:block",
+ "tooltip-content absolute whitespace-nowrap hidden group-hover/tooltip:block fixed",
"z-50 w-auto overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
@variant_class,
@class
diff --git a/lib/utils/cache.ex b/lib/utils/cache.ex
deleted file mode 100644
index 74aa202..0000000
--- a/lib/utils/cache.ex
+++ /dev/null
@@ -1,44 +0,0 @@
-defmodule SaladUI.Cache do
- @moduledoc false
- use GenServer
-
- alias SaladUI.Merge.ClassTree
-
- @default_table_name :turboprop_cache
-
- @doc false
- def default_table_name, do: @default_table_name
-
- def start_link(default) when is_list(default) do
- GenServer.start_link(__MODULE__, default)
- end
-
- @impl true
- def init(opts \\ []) do
- table_name = Keyword.get(opts, :cache_table_name, @default_table_name)
- create_table(table_name)
-
- insert(:class_tree, ClassTree.generate())
-
- {:ok, []}
- end
-
- def create_table(table_name \\ @default_table_name) do
- :ets.new(table_name, [:set, :public, :named_table, read_concurrency: true])
- end
-
- def insert(key, value, table_name \\ @default_table_name) do
- :ets.insert(table_name, {key, value})
- end
-
- def retrieve(key, table_name \\ @default_table_name) do
- case :ets.lookup(table_name, key) do
- [{^key, value}] -> value
- [] -> nil
- end
- end
-
- def purge(table_name \\ @default_table_name) do
- :ets.delete_all_objects(table_name)
- end
-end
diff --git a/lib/utils/merge.ex b/lib/utils/merge.ex
deleted file mode 100644
index c8913ca..0000000
--- a/lib/utils/merge.ex
+++ /dev/null
@@ -1,141 +0,0 @@
-defmodule SaladUI.Merge do
- @moduledoc """
- SaladUI Merge adds efficient class joining and merging of TailwindCSS classes to Elixir.
-
- TailwindCSS class names are composable and allow specifying an infinite amount of different styles. Most components allow overriding class
- names, like as passing `class` attribute that then gets merged with the existing styles. This can result in class lists such as
- `text-white bg-red-500 bg-blue-300` where `text-white bg-red-500` is the preset style, and `bg-blue-300` is the override for that one
- specific button that needs to look slightly different.
- Styles based on class are applied according to the order _they are defined at in the stylesheet_. In this example, because TailwindCSS
- orders color definitions alphabetically, the override does not work. `blue` is defined before `red`, so the `bg-red-500` class takes
- precedence since it was defined later.
-
- In order to still allow overriding styles, SaladUI Merge traverses the entire class list, creates a list of all classes and which
- conflicting groups of styles exist in them and gives precedence to the ones that were defined last _in the class list_, which, unlike the
- stylesheet, is in control of the user.
-
-
- ## Example
-
- ```elixir
- iex> merge("text-white bg-red-500 bg-blue-300")
- "text-white bg-blue-300"
-
- iex> merge(["px-2 py-1 bg-red hover:bg-dark-red", "p-3 bg-[#B91C1C]"])
- "hover:bg-dark-red p-3 bg-[#B91C1C]"
- ```
-
- ## Configuration
-
- SaladUI Merge does not currently support full theme configuration - that's on the roadmap!
-
- The limited configuration at the moment is adding Tailwind's `prefix` option.
-
- ```elixir
- config :turboprop,
- prefix: "tw-"
- ```
- """
-
- alias SaladUI.Cache
- alias SaladUI.Merge.Class
- alias SaladUI.Merge.Config
-
- @doc """
- Joins and merges a list of classes.
-
- Passes the input to `join/1` before merging.
- """
- @spec merge(list(), term()) :: binary()
- def merge(input, config \\ Config.config()) do
- input
- |> join()
- |> retrieve_from_cache_or_merge(config)
- end
-
- @doc """
- Joins a list of classes.
- """
- @spec merge(binary() | list()) :: binary()
- def join(input) when is_binary(input), do: input
- def join(input) when is_list(input), do: do_join(input, "")
- def join(_), do: ""
-
- defp do_join("", result), do: result
- defp do_join(nil, result), do: result
- defp do_join([], result), do: result
-
- defp do_join(string, result) when is_binary(string), do: do_join([string], result)
-
- defp do_join([head | tail], result) do
- case to_value(head) do
- "" -> do_join(tail, result)
- value when result == "" -> do_join(tail, value)
- value -> do_join(tail, result <> " " <> value)
- end
- end
-
- defp to_value(value) when is_binary(value), do: value
-
- defp to_value(values) when is_list(values) do
- Enum.reduce(values, "", fn v, acc ->
- case to_value(v) do
- "" -> acc
- resolved_value when acc == "" -> resolved_value
- resolved_value -> acc <> " " <> resolved_value
- end
- end)
- end
-
- defp to_value(_), do: ""
-
- defp retrieve_from_cache_or_merge(classes, config) do
- case Cache.retrieve("merge:#{classes}") do
- nil ->
- merged_classes = do_merge(classes, config)
- Cache.insert("merge:#{classes}", merged_classes)
- merged_classes
-
- merged_classes ->
- merged_classes
- end
- end
-
- defp do_merge(classes, config) do
- classes
- |> String.trim()
- |> String.split(~r/\s+/)
- |> Enum.map(&Class.parse/1)
- |> Enum.reverse()
- |> Enum.reduce(%{classes: [], groups: []}, fn class, acc ->
- handle_class(class, acc, config)
- end)
- |> Map.get(:classes)
- |> Enum.join(" ")
- end
-
- defp handle_class(%{raw: raw, tailwind?: false}, acc, _config), do: Map.update!(acc, :classes, fn classes -> [raw | classes] end)
-
- defp handle_class(%{conflict_id: conflict_id} = class, acc, config) do
- if Enum.member?(acc.groups, conflict_id), do: acc, else: add_class(acc, class, config)
- end
-
- defp add_class(acc, %{raw: raw, group: group, conflict_id: conflict_id, modifier_id: modifier_id}, config) do
- conflicting_groups =
- group
- |> conflicting_groups(config)
- |> Enum.map(&"#{modifier_id}:#{&1}")
- |> then(&[conflict_id | &1])
-
- acc
- |> Map.update!(:classes, fn classes -> [raw | classes] end)
- |> Map.update!(:groups, fn groups -> groups ++ conflicting_groups end)
- end
-
- defp conflicting_groups(group, config) do
- conflicts = Map.get(config.conflicting_groups, group, [])
- modifier_conflicts = Map.get(config.conflicting_group_modifiers, group, [])
-
- conflicts ++ modifier_conflicts
- end
-end
diff --git a/lib/utils/merge/class.ex b/lib/utils/merge/class.ex
deleted file mode 100644
index 045ab80..0000000
--- a/lib/utils/merge/class.ex
+++ /dev/null
@@ -1,115 +0,0 @@
-defmodule SaladUI.Merge.Class do
- @moduledoc false
-
- defstruct [:raw, :tailwind?, :group, :conflict_id, :modifiers, :modifier_id, :important?, :base]
-
- def parse(raw) do
- with {:ok, parsed, "", _, _, _} <- SaladUI.Merge.Parser.class(raw),
- base when is_binary(base) <- Keyword.get(parsed, :base),
- group when is_binary(group) <- group(base),
- modifier_id when is_binary(modifier_id) <- modifier_id(parsed) do
- %__MODULE__{
- raw: raw,
- tailwind?: true,
- group: group,
- conflict_id: "#{modifier_id}:#{group}",
- modifiers: Keyword.get_values(parsed, :modifier),
- modifier_id: modifier_id,
- base: base,
- important?: Keyword.has_key?(parsed, :important)
- }
- else
- _ ->
- %__MODULE__{
- raw: raw,
- tailwind?: false
- }
- end
- end
-
- defp group(class) do
- # Don't consider negation prefix.
- parts =
- case String.split(class, "-") do
- ["" | parts] -> parts
- parts -> parts
- end
-
- group_recursive(parts, SaladUI.Merge.ClassTree.get()) || get_group_id_for_arbitrary_property(class)
- end
-
- defp group_recursive([], %{"group" => group}), do: group
- defp group_recursive([], %{"next" => %{"" => %{"group" => group}}}), do: group
-
- defp group_recursive([part | rest] = path, map) do
- if next = Map.get(map["next"], part) do
- group_recursive(rest, next)
- else
- if Enum.any?(map["validators"]) do
- path = Enum.join(path, "-")
-
- case Enum.find(sort_validators(map["validators"]), fn {_key, %{"function" => validator}} -> validator.(path) end) do
- nil -> nil
- {_key, %{"group" => group}} -> group
- end
- end
- end
- end
-
- defp group_recursive(_, _), do: nil
-
- defp sort_validators(map) do
- map
- |> Enum.to_list()
- |> Enum.reject(fn {key, _value} -> key == "any?" end)
- |> Enum.sort()
- |> append_any?(map)
- end
-
- defp append_any?(list, map) do
- case map["any?"] do
- nil -> list
- any? -> list ++ [{"any?", any?}]
- end
- end
-
- defp get_group_id_for_arbitrary_property(class_name) do
- arbitrary_property_regex = ~r/^\[(.+)\]$/
-
- if Regex.match?(arbitrary_property_regex, class_name) do
- [_, arbitrary_property_class_name] = Regex.run(arbitrary_property_regex, class_name)
-
- property =
- arbitrary_property_class_name
- |> String.split(":", parts: 2)
- |> List.first()
-
- if property do
- ".." <> property
- end
- end
- end
-
- defp modifier_id(parsed_class) do
- modifiers = parsed_class |> Keyword.get_values(:modifier) |> sort_modifiers() |> Enum.join(":")
- if Keyword.has_key?(parsed_class, :important), do: modifiers <> "!", else: modifiers
- end
-
- @doc """
- Sort modifiers according to the following rules:
-
- - All known modifiers are sorted alphabetically.
- - Arbitrary modifiers retain their position.
- """
- def sort_modifiers(modifiers) do
- modifiers
- |> Enum.reduce({[], []}, fn modifier, {sorted, unsorted} ->
- if String.starts_with?(modifier, "[") do
- {sorted ++ Enum.sort(unsorted) ++ [modifier], []}
- else
- {sorted, unsorted ++ [modifier]}
- end
- end)
- |> then(fn {sorted, unsorted} -> sorted ++ Enum.sort(unsorted) end)
- end
-end
diff --git a/lib/utils/merge/class_tree.ex b/lib/utils/merge/class_tree.ex
deleted file mode 100644
index 3d103e0..0000000
--- a/lib/utils/merge/class_tree.ex
+++ /dev/null
@@ -1,123 +0,0 @@
-defmodule SaladUI.Merge.ClassTree do
- @moduledoc false
-
- alias SaladUI.Cache
- alias SaladUI.Merge.Config
-
- def get do
- case Cache.retrieve(:class_tree) do
- nil ->
- class_tree = generate()
- Cache.insert(:class_tree, class_tree)
- class_tree
-
- class_tree ->
- class_tree
- end
- end
-
- def generate do
- config = Config.config()
- theme = Map.get(config, :theme, %{})
- prefix = Map.get(config, :prefix)
- groups = Map.get(config, :groups, [])
-
- groups
- |> Enum.reduce(%{}, fn {group, next}, acc ->
- DeepMerge.deep_merge(acc, handle_next(next, theme, group))
- end)
- |> add_prefix(prefix)
- end
-
- defp handle_next(next, theme, group) do
- Enum.reduce(next, %{"next" => %{}, "validators" => %{}}, fn part, acc ->
- case part do
- {sub_group, sub_next} ->
- add_group(sub_group, sub_next, theme, group, acc)
-
- part ->
- add_part(part, theme, group, acc)
- end
- end)
- end
-
- defp add_part(part, _theme, group, acc) when is_binary(part) do
- next =
- part
- |> String.split("-")
- |> case do
- [] -> %{}
- [single] -> %{single => %{"group" => group, "next" => %{}, "validators" => %{}}}
- multiple -> add_part_recursive(multiple, group)
- end
-
- Map.update(acc, "next", %{}, &DeepMerge.deep_merge(&1, next))
- end
-
- defp add_part(part, theme, group, acc) when is_map(part) do
- DeepMerge.deep_merge(acc, handle_next(part, theme, group))
- end
-
- defp add_part(part, theme, group, acc) when is_function(part) do
- if from_theme?(part) do
- flattened_theme = flatten_theme(part, theme, group)
-
- acc
- |> Map.update("next", %{}, &DeepMerge.deep_merge(&1, flattened_theme["next"]))
- |> Map.update("validators", %{}, &DeepMerge.deep_merge(&1, flattened_theme["validators"]))
- else
- {:name, key} = Function.info(part, :name)
-
- Map.update(acc, "validators", %{}, fn validators ->
- Map.put(validators, to_string(key), %{"function" => part, "group" => group})
- end)
- end
- end
-
- defp add_part_recursive([single], group) do
- %{single => %{"group" => group, "next" => %{}, "validators" => %{}}}
- end
-
- defp add_part_recursive([current | rest], group) do
- %{current => %{"next" => add_part_recursive(rest, group)}}
- end
-
- defp add_group(sub_group, sub_next, theme, group, acc) do
- sub_group
- |> String.split("-")
- |> add_nested_group(sub_next, theme, group, acc)
- end
-
- defp add_nested_group([current | rest], sub_next, theme, group, acc) do
- Map.update(acc, "next", %{}, &Map.put(&1, current, add_nested_group(rest, List.flatten(sub_next), theme, group, acc)))
- end
-
- defp add_nested_group([], sub_next, theme, group, _acc) do
- handle_next(sub_next, theme, group)
- end
-
- defp from_theme?(function), do: Function.info(function, :module) == {:module, Config}
-
- defp flatten_theme(function, theme, group, acc \\ %{"next" => %{}, "validators" => %{}}) do
- Enum.reduce(function.(theme), acc, fn
- item, acc when is_binary(item) ->
- Map.update(acc, "next", %{}, &Map.put(&1, item, %{"next" => %{}, "validators" => %{}, "group" => group}))
-
- item, acc when is_function(item) ->
- if from_theme?(item) do
- flatten_theme(item, theme, group, acc)
- else
- Map.update(acc, "validators", %{}, fn validators ->
- {:name, key} = Function.info(item, :name)
- Map.put(validators, to_string(key), %{"function" => item, "group" => group})
- end)
- end
- end)
- end
-
- defp add_prefix(groups, nil), do: groups
-
- defp add_prefix(groups, prefix) do
- %{"next" => %{String.trim_trailing(prefix, "-") => groups}, "validators" => %{}}
- end
-end
diff --git a/lib/utils/merge/config.ex b/lib/utils/merge/config.ex
deleted file mode 100644
index db45d89..0000000
--- a/lib/utils/merge/config.ex
+++ /dev/null
@@ -1,686 +0,0 @@
-defmodule SaladUI.Merge.Config do
- @moduledoc false
- import SaladUI.Merge.Validator
-
- @theme_groups ~w(colors spacing blur brightness borderColor borderRadius borderSpacing borderWidth contrast grayscale hueRotate invert gap gradientColorStops gradientColorStopPositions inset margin opacity padding saturate scale sepia skew space translate)
-
- def config do
- prefix = Application.get_env(:turboprop, :prefix)
- theme = Application.get_env(:turboprop, :theme, default_theme())
-
- Map.merge(%{prefix: prefix, theme: theme}, settings())
- end
-
- defp default_theme do
- %{
- colors: [&any?/1],
- spacing: [&length?/1, &arbitrary_length?/1],
- blur: ["none", "", &tshirt_size?/1, &arbitrary_value?/1],
- brightness: number(),
- borderColor: [&from_theme(&1, "colors")],
- borderRadius: ["none", "", "full", &tshirt_size?/1, &arbitrary_value?/1],
- borderSpacing: spacing_with_arbitrary(),
- borderWidth: length_with_empty_and_arbitrary(),
- contrast: number(),
- grayscale: zero_and_empty(),
- hueRotate: number_and_arbitrary(),
- invert: zero_and_empty(),
- gap: spacing_with_arbitrary(),
- gradientColorStops: [&from_theme(&1, "colors")],
- gradientColorStopPositions: [&percent?/1, &arbitrary_length?/1],
- inset: spacing_with_auto_and_arbitrary(),
- margin: spacing_with_auto_and_arbitrary(),
- opacity: number(),
- padding: spacing_with_arbitrary(),
- saturate: number(),
- scale: number(),
- sepia: zero_and_empty(),
- skew: number_and_arbitrary(),
- space: spacing_with_arbitrary(),
- translate: spacing_with_arbitrary()
- }
- end
-
- defp settings do
- %{
- groups: %{
- "aspect" => %{"aspect" => ["auto", "square", "video", &arbitrary_value?/1]},
- "container" => ["container"],
- "columns" => %{"columns" => [&tshirt_size?/1]},
- "break-after" => %{"break-after" => breaks()},
- "break-before" => %{"break-before" => breaks()},
- "break-inside" => %{"break-inside" => ["auto", "avoid", "avoid-page", "avoid-column"]},
- "box-decoration" => %{"box-decoration" => ["slice", "clone"]},
- "box" => %{"box" => ["border", "content"]},
- "display" => [
- "block",
- "inline-block",
- "inline",
- "flex",
- "inline-flex",
- "table",
- "inline-table",
- "table-caption",
- "table-cell",
- "table-column",
- "table-column-group",
- "table-footer-group",
- "table-header-group",
- "table-row-group",
- "table-row",
- "flow-root",
- "grid",
- "inline-grid",
- "contents",
- "list-item",
- "hidden"
- ],
- "float" => %{"float" => ["right", "left", "none", "start", "end"]},
- "clear" => %{"clear" => ["left", "right", "both", "none", "start", "end"]},
- "isolation" => ["isolate", "isolation-auto"],
- "object-fit" => %{"object" => ["contain", "cover", "fill", "none", "scale-down"]},
- "object-position" => %{"object" => [positions(), &arbitrary_value?/1]},
- "overflow" => %{"overflow" => overflow()},
- "overflow-x" => %{"overflow-x" => overflow()},
- "overflow-y" => %{"overflow-y" => overflow()},
- "overscroll" => %{"overscroll" => overscroll()},
- "overscroll-x" => %{"overscroll-x" => overscroll()},
- "overscroll-y" => %{"overscroll-y" => overscroll()},
- "position" => ["static", "fixed", "absolute", "relative", "sticky"],
- "inset" => %{"inset" => [&from_theme(&1, "inset")]},
- "inset-x" => %{"inset-x" => [&from_theme(&1, "inset")]},
- "inset-y" => %{"inset-y" => [&from_theme(&1, "inset")]},
- "start" => %{"start" => [&from_theme(&1, "inset")]},
- "end" => %{"end" => [&from_theme(&1, "inset")]},
- "top" => %{"top" => [&from_theme(&1, "inset")]},
- "right" => %{"right" => [&from_theme(&1, "inset")]},
- "bottom" => %{"bottom" => [&from_theme(&1, "inset")]},
- "left" => %{"left" => [&from_theme(&1, "inset")]},
- "visibility" => ["visible", "invisible", "collapse"],
- "z" => %{"z" => ["auto", &integer?/1, &arbitrary_value?/1]},
- "basis" => %{"basis" => spacing_with_auto_and_arbitrary()},
- "flex-direction" => %{"flex" => ["row", "row-reverse", "col", "col-reverse"]},
- "flex-wrap" => %{"flex" => ["wrap", "wrap-reverse", "nowrap"]},
- "flex" => %{"flex" => ["1", "auto", "initial", "none", &arbitrary_value?/1]},
- "grow" => %{"grow" => zero_and_empty()},
- "shrink" => %{"shrink" => zero_and_empty()},
- "order" => %{"order" => ["first", "last", "none", &integer?/1, &arbitrary_value?/1]},
- "grid-cols" => %{"grid-cols" => [&any?/1]},
- "col-start-end" => %{"col" => ["auto", %{"span" => ["full", &integer?/1, &arbitrary_value?/1]}, &arbitrary_value?/1]},
- "col-start" => %{"col-start" => number_with_auto_and_arbitrary()},
- "col-end" => %{"col-end" => number_with_auto_and_arbitrary()},
- "grid-rows" => %{"grid-rows" => [&any?/1]},
- "row-start-end" => %{"row" => ["auto", %{"span" => ["full", &integer?/1, &arbitrary_value?/1]}, &arbitrary_value?/1]},
- "row-start" => %{"row-start" => number_with_auto_and_arbitrary()},
- "row-end" => %{"row-end" => number_with_auto_and_arbitrary()},
- "grid-flow" => %{"grid-flow" => ["row", "col", "dense", "row-dense", "col-dense"]},
- "auto-cols" => %{"auto-cols" => ["auto", "min", "max", "fr", &arbitrary_value?/1]},
- "auto-rows" => %{"auto-rows" => ["auto", "min", "max", "fr", &arbitrary_value?/1]},
- "gap" => %{"gap" => [&from_theme(&1, "gap")]},
- "gap-x" => %{"gap-x" => [&from_theme(&1, "gap")]},
- "gap-y" => %{"gap-y" => [&from_theme(&1, "gap")]},
- "justify-content" => %{"justify" => ["normal", align()]},
- "justify-items" => %{"justify-items" => ["start", "end", "center", "stretch"]},
- "justify-self" => %{"justify-self" => ["auto", "start", "end", "center", "stretch"]},
- "align-content" => %{"content" => ["normal", align(), "baseline"]},
- "align-items" => %{"items" => ["start", "end", "center", "baseline", "stretch"]},
- "align-self" => %{"self" => ["auto", "start", "end", "center", "stretch", "baseline"]},
- "place-content" => %{"place-content" => [align(), "baseline"]},
- "place-items" => %{"place-items" => ["start", "end", "center", "baseline", "stretch"]},
- "place-self" => %{"place-self" => ["auto", "start", "end", "center", "stretch"]},
- "p" => %{"p" => [&from_theme(&1, "padding")]},
- "px" => %{"px" => [&from_theme(&1, "padding")]},
- "py" => %{"py" => [&from_theme(&1, "padding")]},
- "ps" => %{"ps" => [&from_theme(&1, "padding")]},
- "pe" => %{"pe" => [&from_theme(&1, "padding")]},
- "pt" => %{"pt" => [&from_theme(&1, "padding")]},
- "pr" => %{"pr" => [&from_theme(&1, "padding")]},
- "pb" => %{"pb" => [&from_theme(&1, "padding")]},
- "pl" => %{"pl" => [&from_theme(&1, "padding")]},
- "m" => %{"m" => [&from_theme(&1, "margin")]},
- "mx" => %{"mx" => [&from_theme(&1, "margin")]},
- "my" => %{"my" => [&from_theme(&1, "margin")]},
- "ms" => %{"ms" => [&from_theme(&1, "margin")]},
- "me" => %{"me" => [&from_theme(&1, "margin")]},
- "mt" => %{"mt" => [&from_theme(&1, "margin")]},
- "mr" => %{"mr" => [&from_theme(&1, "margin")]},
- "mb" => %{"mb" => [&from_theme(&1, "margin")]},
- "ml" => %{"ml" => [&from_theme(&1, "margin")]},
- "space-x" => %{"space-x" => [&from_theme(&1, "space")]},
- "space-x-reverse" => ["space-x-reverse"],
- "space-y" => %{"space-y" => [&from_theme(&1, "space")]},
- "space-y-reverse" => ["space-y-reverse"],
- "w" => %{"w" => ["auto", "min", "max", "fit", "svw", "lvw", "dvw", &from_theme(&1, "spacing"), &arbitrary_value?/1]},
- "min-w" => %{"min-w" => ["min", "max", "fit", &from_theme(&1, "spacing"), &arbitrary_value?/1]},
- "max-w" => %{
- "max-w" => [
- "none",
- "full",
- "min",
- "max",
- "fit",
- "prose",
- &tshirt_size?/1,
- &arbitrary_value?/1,
- &from_theme(&1, "spacing"),
- %{"screen" => [&tshirt_size?/1]}
- ]
- },
- "h" => %{
- "h" => [
- "auto",
- "min",
- "max",
- "fit",
- "svh",
- "lvh",
- "dvh",
- &from_theme(&1, "spacing"),
- &arbitrary_value?/1
- ]
- },
- "min-h" => %{"min-h" => ["min", "max", "fit", "svh", "lvh", "dvh", &from_theme(&1, "spacing"), &arbitrary_value?/1]},
- "max-h" => %{"max-h" => ["min", "max", "fit", "svh", "lvh", "dvh", &from_theme(&1, "spacing"), &arbitrary_value?/1]},
- "size" => %{"size" => ["auto", "min", "max", "fit", &from_theme(&1, "spacing"), &arbitrary_value?/1]},
- "font-size" => %{"text" => ["base", &tshirt_size?/1, &arbitrary_length?/1]},
- "font-smoothing" => ["antialiased", "subpixel-antialiased"],
- "font-style" => ["italic", "not-italic"],
- "font-weight" => %{
- "font" => [
- "thin",
- "extralight",
- "light",
- "normal",
- "medium",
- "semibold",
- "bold",
- "extrabold",
- "black",
- &arbitrary_number?/1
- ]
- },
- "font-family" => %{"font" => [&any?/1]},
- "fvn-normal" => ["normal-nums"],
- "fvn-ordinal" => ["ordinal"],
- "fvn-slashed-zero" => ["slashed-zero"],
- "fvn-figure" => ["lining-nums", "oldstyle-nums"],
- "fvn-spacing" => ["proportional-nums", "tabular-nums"],
- "fvn-fraction" => ["diagonal-fractions", "stacked-fractions"],
- "tracking" => %{
- "tracking" => [
- "tighter",
- "tight",
- "normal",
- "wide",
- "wider",
- "widest",
- &arbitrary_value?/1
- ]
- },
- "line-clamp" => %{"line-clamp" => ["none", &number?/1, &arbitrary_number?/1]},
- "leading" => %{
- "leading" => [
- "none",
- "tight",
- "snug",
- "normal",
- "relaxed",
- "loose",
- &length?/1,
- &arbitrary_value?/1
- ]
- },
- "list-image" => %{"list-image" => ["none", &arbitrary_value?/1]},
- "list-style-type" => %{"list" => ["none", "disc", "decimal", &arbitrary_value?/1]},
- "list-style-position" => %{"list" => ["inside", "outside"]},
- "placeholder-color" => %{"placeholder" => [&from_theme(&1, "colors")]},
- "placeholder-opacity" => %{"placeholder-opacity" => [&from_theme(&1, "opacity")]},
- "text-alignment" => %{"text" => ["left", "center", "right", "justify", "start", "end"]},
- "text-color" => %{"text" => [&from_theme(&1, "colors")]},
- "text-opacity" => %{"text-opacity" => [&from_theme(&1, "opacity")]},
- "text-decoration" => ["underline", "overline", "line-through", "no-underline"],
- "text-decoration-style" => %{"decoration" => [line_styles(), "wavy"]},
- "text-decoration-thickness" => %{"decoration" => ["auto", "from-font", &length?/1, &arbitrary_length?/1]},
- "underline-offset" => %{"underline-offset" => ["auto", &length?/1, &arbitrary_value?/1]},
- "text-decoration-color" => %{"decoration" => [&from_theme(&1, "colors")]},
- "text-transform" => ["uppercase", "lowercase", "capitalize", "normal-case"],
- "text-overflow" => ["truncate", "text-ellipsis", "text-clip"],
- "text-wrap" => %{"text" => ["wrap", "nowrap", "balance", "pretty"]},
- "indent" => %{"indent" => spacing_with_arbitrary()},
- "vertical-align" => %{
- "align" => [
- "baseline",
- "top",
- "middle",
- "bottom",
- "text-top",
- "text-bottom",
- "sub",
- "super",
- &arbitrary_value?/1
- ]
- },
- "whitespace" => %{"whitespace" => ["normal", "nowrap", "pre", "pre-line", "pre-wrap", "break-spaces"]},
- "break" => %{"break" => ["normal", "words", "all", "keep"]},
- "hyphens" => %{"hyphens" => ["none", "manual", "auto"]},
- "content" => %{"content" => ["none", &arbitrary_value?/1]},
- "bg-attachment" => %{"bg" => ["fixed", "local", "scroll"]},
- "bg-clip" => %{"bg-clip" => ["border", "padding", "content", "text"]},
- "bg-opacity" => %{"bg-opacity" => [&from_theme(&1, "opacity")]},
- "bg-origin" => %{"bg-origin" => ["border", "padding", "content"]},
- "bg-position" => %{"bg" => [positions(), &arbitrary_position?/1]},
- "bg-repeat" => %{"bg" => ["no-repeat", %{"repeat" => ["", "x", "y", "round", "space"]}]},
- "bg-size" => %{"bg" => ["auto", "cover", "contain", &arbitrary_size?/1]},
- "bg-image" => %{
- "bg" => [
- "none",
- %{"gradient-to" => ["t", "tr", "r", "br", "b", "bl", "l", "tl"]},
- &arbitrary_image?/1
- ]
- },
- "bg-color" => %{"bg" => [&from_theme(&1, "colors")]},
- "gradient-from-pos" => %{"from" => [&from_theme(&1, "gradientColorStopPositions")]},
- "gradient-via-pos" => %{"via" => [&from_theme(&1, "gradientColorStopPositions")]},
- "gradient-to-pos" => %{"to" => [&from_theme(&1, "gradientColorStopPositions")]},
- "gradient-from" => %{"from" => [&from_theme(&1, "gradientColorStops")]},
- "gradient-via" => %{"via" => [&from_theme(&1, "gradientColorStops")]},
- "gradient-to" => %{"to" => [&from_theme(&1, "gradientColorStops")]},
- "rounded" => %{"rounded" => [&from_theme(&1, "borderRadius")]},
- "rounded-s" => %{"rounded-s" => [&from_theme(&1, "borderRadius")]},
- "rounded-e" => %{"rounded-e" => [&from_theme(&1, "borderRadius")]},
- "rounded-t" => %{"rounded-t" => [&from_theme(&1, "borderRadius")]},
- "rounded-r" => %{"rounded-r" => [&from_theme(&1, "borderRadius")]},
- "rounded-b" => %{"rounded-b" => [&from_theme(&1, "borderRadius")]},
- "rounded-l" => %{"rounded-l" => [&from_theme(&1, "borderRadius")]},
- "rounded-ss" => %{"rounded-ss" => [&from_theme(&1, "borderRadius")]},
- "rounded-se" => %{"rounded-se" => [&from_theme(&1, "borderRadius")]},
- "rounded-ee" => %{"rounded-ee" => [&from_theme(&1, "borderRadius")]},
- "rounded-es" => %{"rounded-es" => [&from_theme(&1, "borderRadius")]},
- "rounded-tl" => %{"rounded-tl" => [&from_theme(&1, "borderRadius")]},
- "rounded-tr" => %{"rounded-tr" => [&from_theme(&1, "borderRadius")]},
- "rounded-br" => %{"rounded-br" => [&from_theme(&1, "borderRadius")]},
- "rounded-bl" => %{"rounded-bl" => [&from_theme(&1, "borderRadius")]},
- "border-w" => %{"border" => [&from_theme(&1, "borderWidth")]},
- "border-w-x" => %{"border-x" => [&from_theme(&1, "borderWidth")]},
- "border-w-y" => %{"border-y" => [&from_theme(&1, "borderWidth")]},
- "border-w-s" => %{"border-s" => [&from_theme(&1, "borderWidth")]},
- "border-w-e" => %{"border-e" => [&from_theme(&1, "borderWidth")]},
- "border-w-t" => %{"border-t" => [&from_theme(&1, "borderWidth")]},
- "border-w-r" => %{"border-r" => [&from_theme(&1, "borderWidth")]},
- "border-w-b" => %{"border-b" => [&from_theme(&1, "borderWidth")]},
- "border-w-l" => %{"border-l" => [&from_theme(&1, "borderWidth")]},
- "border-opacity" => %{"border-opacity" => [&from_theme(&1, "opacity")]},
- "border-style" => %{"border" => [line_styles(), "hidden"]},
- "divide-x" => %{"divide-x" => [&from_theme(&1, "borderWidth")]},
- "divide-x-reverse" => ["divide-x-reverse"],
- "divide-y" => %{"divide-y" => [&from_theme(&1, "borderWidth")]},
- "divide-y-reverse" => ["divide-y-reverse"],
- "divide-opacity" => %{"divide-opacity" => [&from_theme(&1, "opacity")]},
- "divide-style" => %{"divide" => line_styles()},
- "border-color" => %{"border" => [&from_theme(&1, "borderColor")]},
- "border-color-x" => %{"border-x" => [&from_theme(&1, "borderColor")]},
- "border-color-y" => %{"border-y" => [&from_theme(&1, "borderColor")]},
- "border-color-t" => %{"border-t" => [&from_theme(&1, "borderColor")]},
- "border-color-r" => %{"border-r" => [&from_theme(&1, "borderColor")]},
- "border-color-b" => %{"border-b" => [&from_theme(&1, "borderColor")]},
- "border-color-l" => %{"border-l" => [&from_theme(&1, "borderColor")]},
- "divide-color" => %{"divide" => [&from_theme(&1, "borderColor")]},
- "outline-style" => %{"outline" => ["", line_styles()]},
- "outline-offset" => %{"outline-offset" => [&length?/1, &arbitrary_value?/1]},
- "outline-w" => %{"outline" => [&length?/1, &arbitrary_length?/1]},
- "outline-color" => %{"outline" => [&from_theme(&1, "colors")]},
- "ring-w" => %{"ring" => length_with_empty_and_arbitrary()},
- "ring-w-inset" => ["ring-inset"],
- "ring-color" => %{"ring" => [&from_theme(&1, "colors")]},
- "ring-opacity" => %{"ring-opacity" => [&from_theme(&1, "opacity")]},
- "ring-offset-w" => %{"ring-offset" => [&length?/1, &arbitrary_length?/1]},
- "ring-offset-color" => %{"ring-offset" => [&from_theme(&1, "colors")]},
- "shadow" => %{"shadow" => ["", "inner", "none", &tshirt_size?/1, &arbitrary_shadow?/1]},
- "shadow-color" => %{"shadow" => [&any?/1]},
- "opacity" => %{"opacity" => [&from_theme(&1, "opacity")]},
- "mix-blend" => %{"mix-blend" => [blend_modes(), "plus-lighter", "plus-darker"]},
- "bg-blend" => %{"bg-blend" => blend_modes()},
- "filter" => %{"filter" => ["", "none"]},
- "blur" => %{"blur" => [&from_theme(&1, "blur")]},
- "brightness" => %{"brightness" => [&from_theme(&1, "brightness")]},
- "contrast" => %{"contrast" => [&from_theme(&1, "contrast")]},
- "drop-shadow" => %{"drop-shadow" => ["", "none", &tshirt_size?/1, &arbitrary_value?/1]},
- "grayscale" => %{"grayscale" => [&from_theme(&1, "grayscale")]},
- "hue-rotate" => %{"hue-rotate" => [&from_theme(&1, "hueRotate")]},
- "invert" => %{"invert" => [&from_theme(&1, "invert")]},
- "saturate" => %{"saturate" => [&from_theme(&1, "saturate")]},
- "sepia" => %{"sepia" => [&from_theme(&1, "sepia")]},
- "backdrop-filter" => %{"backdrop-filter" => ["", "none"]},
- "backdrop-blur" => %{"backdrop-blur" => [&from_theme(&1, "blur")]},
- "backdrop-brightness" => %{"backdrop-brightness" => [&from_theme(&1, "brightness")]},
- "backdrop-contrast" => %{"backdrop-contrast" => [&from_theme(&1, "contrast")]},
- "backdrop-grayscale" => %{"backdrop-grayscale" => [&from_theme(&1, "grayscale")]},
- "backdrop-hue-rotate" => %{"backdrop-hue-rotate" => [&from_theme(&1, "hueRotate")]},
- "backdrop-invert" => %{"backdrop-invert" => [&from_theme(&1, "invert")]},
- "backdrop-opacity" => %{"backdrop-opacity" => [&from_theme(&1, "opacity")]},
- "backdrop-saturate" => %{"backdrop-saturate" => [&from_theme(&1, "saturate")]},
- "backdrop-sepia" => %{"backdrop-sepia" => [&from_theme(&1, "sepia")]},
- "border-collapse" => %{"border" => ["collapse", "separate"]},
- "border-spacing" => %{"border-spacing" => [&from_theme(&1, "borderSpacing")]},
- "border-spacing-x" => %{"border-spacing-x" => [&from_theme(&1, "borderSpacing")]},
- "border-spacing-y" => %{"border-spacing-y" => [&from_theme(&1, "borderSpacing")]},
- "table-layout" => %{"table" => ["auto", "fixed"]},
- "caption" => %{"caption" => ["top", "bottom"]},
- "transition" => %{
- "transition" => [
- "none",
- "all",
- "",
- "colors",
- "opacity",
- "shadow",
- "transform",
- &arbitrary_value?/1
- ]
- },
- "duration" => %{"duration" => number_and_arbitrary()},
- "ease" => %{"ease" => ["linear", "in", "out", "in-out", &arbitrary_value?/1]},
- "delay" => %{"delay" => number_and_arbitrary()},
- "animate" => %{"animate" => ["none", "spin", "ping", "pulse", "bounce", &arbitrary_value?/1]},
- "transform" => %{"transform" => ["", "gpu", "none"]},
- "scale" => %{"scale" => [&from_theme(&1, "scale")]},
- "scale-x" => %{"scale-x" => [&from_theme(&1, "scale")]},
- "scale-y" => %{"scale-y" => [&from_theme(&1, "scale")]},
- "rotate" => %{"rotate" => [&integer?/1, &arbitrary_value?/1]},
- "translate-x" => %{"translate-x" => [&from_theme(&1, "translate")]},
- "translate-y" => %{"translate-y" => [&from_theme(&1, "translate")]},
- "skew-x" => %{"skew-x" => [&from_theme(&1, "skew")]},
- "skew-y" => %{"skew-y" => [&from_theme(&1, "skew")]},
- "transform-origin" => %{
- "origin" => [
- "center",
- "top",
- "top-right",
- "right",
- "bottom-right",
- "bottom",
- "bottom-left",
- "left",
- "top-left",
- &arbitrary_value?/1
- ]
- },
- "accent" => %{"accent" => ["auto", &from_theme(&1, "colors")]},
- "appearance" => %{"appearance" => ["none", "auto"]},
- "cursor" => %{
- "cursor" => [
- "auto",
- "default",
- "pointer",
- "wait",
- "text",
- "move",
- "help",
- "not-allowed",
- "none",
- "context-menu",
- "progress",
- "cell",
- "crosshair",
- "vertical-text",
- "alias",
- "copy",
- "no-drop",
- "grab",
- "grabbing",
- "all-scroll",
- "col-resize",
- "row-resize",
- "n-resize",
- "e-resize",
- "s-resize",
- "w-resize",
- "ne-resize",
- "nw-resize",
- "se-resize",
- "sw-resize",
- "ew-resize",
- "ns-resize",
- "nesw-resize",
- "nwse-resize",
- "zoom-in",
- "zoom-out",
- &arbitrary_value?/1
- ]
- },
- "caret-color" => %{"caret" => [&from_theme(&1, "colors")]},
- "pointer-events" => %{"pointer-events" => ["none", "auto"]},
- "resize" => %{"resize" => ["none", "y", "x", ""]},
- "scroll-behavior" => %{"scroll" => ["auto", "smooth"]},
- "scroll-m" => %{"scroll-m" => spacing_with_arbitrary()},
- "scroll-mx" => %{"scroll-mx" => spacing_with_arbitrary()},
- "scroll-my" => %{"scroll-my" => spacing_with_arbitrary()},
- "scroll-ms" => %{"scroll-ms" => spacing_with_arbitrary()},
- "scroll-me" => %{"scroll-me" => spacing_with_arbitrary()},
- "scroll-mt" => %{"scroll-mt" => spacing_with_arbitrary()},
- "scroll-mr" => %{"scroll-mr" => spacing_with_arbitrary()},
- "scroll-mb" => %{"scroll-mb" => spacing_with_arbitrary()},
- "scroll-ml" => %{"scroll-ml" => spacing_with_arbitrary()},
- "scroll-p" => %{"scroll-p" => spacing_with_arbitrary()},
- "scroll-px" => %{"scroll-px" => spacing_with_arbitrary()},
- "scroll-py" => %{"scroll-py" => spacing_with_arbitrary()},
- "scroll-ps" => %{"scroll-ps" => spacing_with_arbitrary()},
- "scroll-pe" => %{"scroll-pe" => spacing_with_arbitrary()},
- "scroll-pt" => %{"scroll-pt" => spacing_with_arbitrary()},
- "scroll-pr" => %{"scroll-pr" => spacing_with_arbitrary()},
- "scroll-pb" => %{"scroll-pb" => spacing_with_arbitrary()},
- "scroll-pl" => %{"scroll-pl" => spacing_with_arbitrary()},
- "snap-align" => %{"snap" => ["start", "end", "center", "align-none"]},
- "snap-stop" => %{"snap" => ["normal", "always"]},
- "snap-type" => %{"snap" => ["none", "x", "y", "both"]},
- "snap-strictness" => %{"snap" => ["mandatory", "proximity"]},
- "touch" => %{"touch" => ["auto", "none", "manipulation"]},
- "touch-x" => %{"touch-pan" => ["x", "left", "right"]},
- "touch-y" => %{"touch-pan" => ["y", "up", "down"]},
- "touch-pz" => ["touch-pinch-zoom"],
- "select" => %{"select" => ["none", "text", "all", "auto"]},
- "will-change" => %{"will-change" => ["auto", "scroll", "contents", "transform", &arbitrary_value?/1]},
- "fill" => %{"fill" => [&from_theme(&1, "colors"), "none"]},
- "stroke-w" => %{"stroke" => [&length?/1, &arbitrary_length?/1, &arbitrary_number?/1]},
- "stroke" => %{"stroke" => [&from_theme(&1, "colors"), "none"]},
- "sr" => ["sr-only", "not-sr-only"],
- "forced-color-adjust" => %{"forced-color-adjust" => ["auto", "none"]}
- },
- conflicting_groups: %{
- "overflow" => ["overflow-x", "overflow-y"],
- "overscroll" => ["overscroll-x", "overscroll-y"],
- "inset" => ["inset-x", "inset-y", "start", "end", "top", "right", "bottom", "left"],
- "inset-x" => ["right", "left"],
- "inset-y" => ["top", "bottom"],
- "flex" => ["basis", "grow", "shrink"],
- "gap" => ["gap-x", "gap-y"],
- "p" => ["px", "py", "ps", "pe", "pt", "pr", "pb", "pl"],
- "px" => ["pr", "pl"],
- "py" => ["pt", "pb"],
- "m" => ["mx", "my", "ms", "me", "mt", "mr", "mb", "ml"],
- "mx" => ["mr", "ml"],
- "my" => ["mt", "mb"],
- "size" => ["w", "h"],
- "font-size" => ["leading"],
- "fvn-normal" => [
- "fvn-ordinal",
- "fvn-slashed-zero",
- "fvn-figure",
- "fvn-spacing",
- "fvn-fraction"
- ],
- "fvn-ordinal" => ["fvn-normal"],
- "fvn-slashed-zero" => ["fvn-normal"],
- "fvn-figure" => ["fvn-normal"],
- "fvn-spacing" => ["fvn-normal"],
- "fvn-fraction" => ["fvn-normal"],
- "line-clamp" => ["display", "overflow"],
- "rounded" => [
- "rounded-s",
- "rounded-e",
- "rounded-t",
- "rounded-r",
- "rounded-b",
- "rounded-l",
- "rounded-ss",
- "rounded-se",
- "rounded-ee",
- "rounded-es",
- "rounded-tl",
- "rounded-tr",
- "rounded-br",
- "rounded-bl"
- ],
- "rounded-s" => ["rounded-ss", "rounded-es"],
- "rounded-e" => ["rounded-se", "rounded-ee"],
- "rounded-t" => ["rounded-tl", "rounded-tr"],
- "rounded-r" => ["rounded-tr", "rounded-br"],
- "rounded-b" => ["rounded-br", "rounded-bl"],
- "rounded-l" => ["rounded-tl", "rounded-bl"],
- "border-spacing" => ["border-spacing-x", "border-spacing-y"],
- "border-w" => [
- "border-w-s",
- "border-w-e",
- "border-w-t",
- "border-w-r",
- "border-w-b",
- "border-w-l"
- ],
- "border-w-x" => ["border-w-r", "border-w-l"],
- "border-w-y" => ["border-w-t", "border-w-b"],
- "border-color" => ["border-color-t", "border-color-r", "border-color-b", "border-color-l"],
- "border-color-x" => ["border-color-r", "border-color-l"],
- "border-color-y" => ["border-color-t", "border-color-b"],
- "scroll-m" => [
- "scroll-mx",
- "scroll-my",
- "scroll-ms",
- "scroll-me",
- "scroll-mt",
- "scroll-mr",
- "scroll-mb",
- "scroll-ml"
- ],
- "scroll-mx" => ["scroll-mr", "scroll-ml"],
- "scroll-my" => ["scroll-mt", "scroll-mb"],
- "scroll-p" => [
- "scroll-px",
- "scroll-py",
- "scroll-ps",
- "scroll-pe",
- "scroll-pt",
- "scroll-pr",
- "scroll-pb",
- "scroll-pl"
- ],
- "scroll-px" => ["scroll-pr", "scroll-pl"],
- "scroll-py" => ["scroll-pt", "scroll-pb"],
- "touch" => ["touch-x", "touch-y", "touch-pz"],
- "touch-x" => ["touch"],
- "touch-y" => ["touch"],
- "touch-pz" => ["touch"]
- },
- conflicting_group_modifiers: %{
- "font-size" => ["leading"]
- }
- }
- end
-
- for group <- @theme_groups do
- defp from_theme(theme, unquote(group)) do
- theme[String.to_existing_atom(unquote(group))] || []
- end
- end
-
- defp overscroll do
- ["auto", "contain", "none"]
- end
-
- defp overflow do
- ["auto", "hidden", "clip", "visible", "scroll"]
- end
-
- defp spacing_with_auto_and_arbitrary do
- ["auto", &arbitrary_value?/1, &from_theme(&1, "spacing")]
- end
-
- defp spacing_with_arbitrary do
- [&arbitrary_value?/1, &from_theme(&1, "spacing")]
- end
-
- defp length_with_empty_and_arbitrary do
- ["", &length?/1, &arbitrary_length?/1]
- end
-
- defp number_with_auto_and_arbitrary do
- ["auto", &number?/1, &arbitrary_value?/1]
- end
-
- defp positions do
- [
- "bottom",
- "center",
- "left",
- "left-bottom",
- "left-top",
- "right",
- "right-bottom",
- "right-top",
- "top"
- ]
- end
-
- defp line_styles do
- ["solid", "dashed", "dotted", "double", "none"]
- end
-
- defp blend_modes do
- [
- "normal",
- "multiply",
- "screen",
- "overlay",
- "darken",
- "lighten",
- "color-dodge",
- "color-burn",
- "hard-light",
- "soft-light",
- "difference",
- "exclusion",
- "hue",
- "saturation",
- "color",
- "luminosity"
- ]
- end
-
- defp align do
- ["start", "end", "center", "between", "around", "evenly", "stretch"]
- end
-
- defp zero_and_empty do
- ["", "0", &arbitrary_value?/1]
- end
-
- defp breaks do
- [
- "auto",
- "avoid",
- "all",
- "avoid-page",
- "page",
- "left",
- "right",
- "column"
- ]
- end
-
- defp number do
- [&number?/1, &arbitrary_number?/1]
- end
-
- defp number_and_arbitrary do
- [&number?/1, &arbitrary_value?/1]
- end
-end
diff --git a/lib/utils/merge/parser.ex b/lib/utils/merge/parser.ex
deleted file mode 100644
index a03dba9..0000000
--- a/lib/utils/merge/parser.ex
+++ /dev/null
@@ -1,46 +0,0 @@
-defmodule SaladUI.Merge.Parser do
- @moduledoc false
- import NimbleParsec
-
- @chars [?a..?z, ?A..?Z, ?0..?9, ?-, ?_, ?., ?,, ?@, ?{, ?}, ?(, ?), ?>, ?*, ?&, ?', ?%, ?#]
-
- regular_chars = ascii_string(@chars, min: 1)
-
- modifier =
- [parsec(:arbitrary), regular_chars]
- |> choice()
- |> times(min: 1)
- |> ignore(string(":"))
- |> reduce({Enum, :join, [""]})
- |> unwrap_and_tag(:modifier)
- |> times(min: 1)
-
- important = "!" |> string() |> unwrap_and_tag(:important)
-
- base =
- [parsec(:arbitrary), regular_chars]
- |> choice()
- |> times(min: 1)
- |> reduce({Enum, :join, [""]})
- |> unwrap_and_tag(:base)
-
- postfix =
- "/"
- |> string()
- |> ignore()
- |> ascii_string([?a..?z, ?0..?9], min: 1)
- |> unwrap_and_tag(:postfix)
-
- defparsec :arbitrary,
- "["
- |> string()
- |> concat(times(choice([parsec(:arbitrary), ascii_string(@chars ++ [?:, ?/], min: 1)]), min: 1))
- |> concat(string("]"))
-
- defparsec :class,
- modifier
- |> optional()
- |> concat(optional(important))
- |> concat(base)
- |> concat(optional(postfix))
-end
diff --git a/lib/utils/merge/validator.ex b/lib/utils/merge/validator.ex
deleted file mode 100644
index 24023ca..0000000
--- a/lib/utils/merge/validator.ex
+++ /dev/null
@@ -1,102 +0,0 @@
-defmodule SaladUI.Merge.Validator do
- @moduledoc false
- @arbitrary_value_regex ~r/^\[(?:([a-z-]+):)?(.+)\]$/i
- @fraction_regex ~r/^\d+\/\d+$/
- @tshirt_size_regex ~r/^(xs|sm|md|lg|(\d+(\.\d+)?)?xl)$/
- @image_regex ~r/^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/
- @color_function_regex ~r/^(rgba?|hsla?|hwb|(ok)?(lab|lch))\(.+\)$/
- @shadow_regex ~r/^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/
- @length_unit_regex ~r/\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/
-
- def arbitrary_value?(value) do
- Regex.match?(@arbitrary_value_regex, value)
- end
-
- def integer?(value) do
- case Integer.parse(value) do
- {_integer, ""} -> true
- _else -> false
- end
- end
-
- def number?(value) do
- # Tailwind allows settings float values without leading zeroes, such as `.01`, which `Float.parse/1` does not support.
- value =
- if Regex.match?(~r/^\.\d+$/, value) do
- "0" <> value
- else
- value
- end
-
- case Float.parse(value) do
- {_float, ""} -> true
- _else -> false
- end
- end
-
- def length?(value) do
- number?(value) or value in ~w(px full screen) or Regex.match?(@fraction_regex, value)
- end
-
- def percent?(value) do
- String.ends_with?(value, "%") and number?(String.slice(value, 0..-2//1))
- end
-
- def tshirt_size?(value) do
- Regex.match?(@tshirt_size_regex, value)
- end
-
- def arbitrary_length?(value) do
- arbitrary_value?(value, "length", &length_only?/1)
- end
-
- def arbitrary_number?(value) do
- arbitrary_value?(value, "number", &number?/1)
- end
-
- def arbitrary_size?(value) do
- arbitrary_value?(value, ~w(length size percentage), &never?/1)
- end
-
- def arbitrary_position?(value) do
- arbitrary_value?(value, "position", &never?/1)
- end
-
- def arbitrary_image?(value) do
- arbitrary_value?(value, ~w(image url), &image?/1)
- end
-
- def image?(value), do: Regex.match?(@image_regex, value)
-
- def length_only?(value), do: Regex.match?(@length_unit_regex, value) and not Regex.match?(@color_function_regex, value)
-
- def arbitrary_shadow?(value) do
- arbitrary_value?(value, "", &shadow?/1)
- end
-
- def any?, do: true
-
- def any?(_value), do: true
-
- def never?(_value), do: false
-
- def shadow?(value), do: Regex.match?(@shadow_regex, value)
-
- def arbitrary_value?(value, label, test_value) do
- case Regex.run(@arbitrary_value_regex, value) do
- [_, label_part, actual_value] ->
- if is_binary(label_part) and label_part != "" do
- case label do
- ^label_part -> true
- list when is_list(list) -> label_part in list
- _ -> false
- end
- else
- test_value.(actual_value)
- end
-
- _ ->
- false
- end
- end
-end
diff --git a/mix.exs b/mix.exs
index 85cf65d..ffe4ee2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -4,7 +4,7 @@ defmodule SaladUI.MixProject do
def project do
[
app: :salad_ui,
- version: "0.13.0",
+ version: "0.13.1",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
@@ -59,8 +59,7 @@ defmodule SaladUI.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
- {:nimble_parsec, "~> 1.0"},
- {:deep_merge, "~> 1.0"},
+ {:tw_merge, "~> 0.1.0"},
{:phoenix_live_view, "~> 0.20.1"},
{:mix_test_watch, "~> 1.2", only: [:dev, :test]},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
diff --git a/mix.lock b/mix.lock
index 1b92bb4..51e705f 100644
--- a/mix.lock
+++ b/mix.lock
@@ -24,6 +24,7 @@
"styler": {:hex, :styler, "0.11.9", "2595393b94e660cd6e8b582876337cc50ff047d184ccbed42fdad2bfd5d78af5", [:mix], [], "hexpm", "8b7806ba1fdc94d0a75127c56875f91db89b75117fcc67572661010c13e1f259"},
"tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
+ "tw_merge": {:hex, :tw_merge, "0.1.0", "26a8ae5e71e0c6818b82b25abb28da06464f0be23bcb4cdce828a4a00867cd5b", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0621fb097a1ff6583dc5b90f66133bf1487f3c3feae0ac27a9b37690e404ac9b"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"},
}
diff --git a/test/salad_ui/accordion_test.exs b/test/salad_ui/accordion_test.exs
index f96c34b..2afca5f 100644
--- a/test/salad_ui/accordion_test.exs
+++ b/test/salad_ui/accordion_test.exs
@@ -101,7 +101,8 @@ defmodule SaladUI.AccordionTest do
assert html =~ ~s(name="my-group")
- for class <- ~w(flex py-4 transition-all items-center justify-between flex-1 font-medium hover:underline custom-class) do
+ for class <-
+ ~w(flex py-4 transition-all items-center justify-between flex-1 font-medium hover:underline custom-class) do
assert html =~ class
end
end
@@ -115,7 +116,8 @@ defmodule SaladUI.AccordionTest do
inner_block: []
})
- for class <- ~w(text-sm overflow-hidden grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 peer-open/accordion:grid-rows-[1fr]) do
+ for class <-
+ ~w(text-sm overflow-hidden grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 peer-open/accordion:grid-rows-[1fr]) do
assert html =~ class
end
diff --git a/test/salad_ui/alert_dialog_test.exs b/test/salad_ui/alert_dialog_test.exs
index b1ae9df..62456f6 100644
--- a/test/salad_ui/alert_dialog_test.exs
+++ b/test/salad_ui/alert_dialog_test.exs
@@ -1,5 +1,6 @@
defmodule SaladUI.AlertDialogTest do
use ComponentCase
+
import SaladUI.AlertDialog
# Helper function to set up assigns
diff --git a/test/salad_ui/alert_test.exs b/test/salad_ui/alert_test.exs
index 45bcf7b..77e41f7 100644
--- a/test/salad_ui/alert_test.exs
+++ b/test/salad_ui/alert_test.exs
@@ -19,6 +19,7 @@ defmodule SaladUI.AlertTest do
for class <- ~w(mb-1 tracking-tight font-medium leading-none) do
assert html =~ class
end
+
assert html =~ "Heads up!"
assert html =~ "Alert Descriptions"
diff --git a/test/salad_ui/avatar_test.exs b/test/salad_ui/avatar_test.exs
index 0b91b9c..9225cac 100644
--- a/test/salad_ui/avatar_test.exs
+++ b/test/salad_ui/avatar_test.exs
@@ -15,6 +15,7 @@ defmodule SaladUI.AvatarTest do
for class <- ~w(aspect-square w-full h-full) do
assert html =~ class
end
+
assert html =~ "src=\"https:\/\/github.com\/shadcn.png\" "
end
@@ -29,6 +30,7 @@ defmodule SaladUI.AvatarTest do
for class <- ~w(flex rounded-full bg-primary text-white items-center justify-center w-full h-full) do
assert html =~ class
end
+
assert html =~ "CN"
end
@@ -43,9 +45,10 @@ defmodule SaladUI.AvatarTest do
""")
- for class <- ~w(flex rounded-full bg-primary text-white items-center justify-center w-full h-full) do
+ for class <- ~w(flex rounded-full bg-primary text-white items-center justify-center w-full h-full) do
assert html =~ class
end
+
assert html =~ "CN"
end
end
diff --git a/test/salad_ui/badge_test.exs b/test/salad_ui/badge_test.exs
index abc2239..45683b6 100644
--- a/test/salad_ui/badge_test.exs
+++ b/test/salad_ui/badge_test.exs
@@ -16,6 +16,7 @@ defmodule SaladUI.BadgeTest do
~w(inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80) do
assert html =~ class
end
+
assert html =~ "Badge"
end
diff --git a/test/salad_ui/collapsible_test.exs b/test/salad_ui/collapsible_test.exs
index 6ae1122..ee578fe 100644
--- a/test/salad_ui/collapsible_test.exs
+++ b/test/salad_ui/collapsible_test.exs
@@ -1,5 +1,6 @@
defmodule SaladUI.CollapsibleTest do
use ComponentCase
+
import SaladUI.Collapsible
describe "collapsible/1" do
@@ -41,13 +42,12 @@ defmodule SaladUI.CollapsibleTest do
html =
rendered_to_string(~H"""
<.collapsible id="test-collapsible">
- <.collapsible_trigger builder={%{id: "test-collapsible", open: false}}>
+ <.collapsible_trigger>
Click me
""")
- assert html =~ ~s(phx-click)
assert html =~ "test-collapsible"
assert html =~ "Click me"
end
@@ -58,7 +58,6 @@ defmodule SaladUI.CollapsibleTest do
html =
rendered_to_string(~H"""
<.collapsible_trigger
- builder={%{id: "test-collapsible", open: false}}
class="custom-trigger-class"
>
Click me
@@ -117,7 +116,7 @@ defmodule SaladUI.CollapsibleTest do
describe "toggle_collapsible/2" do
test "returns JavaScript commands for toggling content" do
- js = toggle_collapsible(%Phoenix.LiveView.JS{}, %{id: "test-collapsible"})
+ js = toggle_collapsible(%Phoenix.LiveView.JS{}, "test-collapsible")
assert js.ops == [
[
@@ -128,7 +127,8 @@ defmodule SaladUI.CollapsibleTest do
outs: [["ease-out"], ["opacity-100"], ["opacity-70"]],
time: 200
}
- ]
+ ],
+ ["toggle_attr", %{attr: ["data-state", "open", "closed"], to: "#test-collapsible"}]
]
end
end
@@ -152,6 +152,5 @@ defmodule SaladUI.CollapsibleTest do
assert html =~ "Hidden Content"
assert html =~ "collapsible-content"
assert html =~ ~s(phx-toggle-collapsible)
- assert html =~ ~s(phx-click)
end
end
diff --git a/test/salad_ui/dialog_test.exs b/test/salad_ui/dialog_test.exs
index 8f1bc8a..d06ae56 100644
--- a/test/salad_ui/dialog_test.exs
+++ b/test/salad_ui/dialog_test.exs
@@ -20,6 +20,7 @@ defmodule SaladUI.DialogTest do
for class <- ~w(flex text-center flex-col space-y-1.5 sm:text-left) do
assert html =~ class
end
+
assert html =~ "This is the content of the header"
for css_class <- ~w(flex flex-col space-y-1.5 text-center sm:text-left) do
diff --git a/test/salad_ui/form_test.exs b/test/salad_ui/form_test.exs
index ad0c015..825a002 100644
--- a/test/salad_ui/form_test.exs
+++ b/test/salad_ui/form_test.exs
@@ -32,6 +32,7 @@ defmodule SaladUI.FormTest do
for class <- ~w(font-medium leading-none text-sm peer-disabled:cursor-not-allowed peer-disabled:opacity-70) do
assert html =~ class
end
+
assert html =~ "This is a label"
end
@@ -74,6 +75,7 @@ defmodule SaladUI.FormTest do
for class <- ~w(text-destructive font-medium text-sm) do
assert html =~ class
end
+
assert html =~ "This is a form message"
end
@@ -111,13 +113,13 @@ defmodule SaladUI.FormTest do
|> rendered_to_string()
|> clean_string()
-
assert html =~
"