defmodule Tails.Custom do @moduledoc """ Use to create a custom tails module, that can be configured with `config :your_app, YourTails` Additionally, themes can be passed at runtime. Example Usage: ```elixir @themes %{ default: %{...}, dark: %{...} } use Tails.Custom, otp_app: :my_app, themes: @themes ``` """ defmacro __using__(opts) do quote location: :keep, generated: true, bind_quoted: [ otp_app: opts[:otp_app], themes: opts[:themes], dark_themes: opts[:dark_themes] ] do require Tails.Custom otp_app = otp_app || :tails if otp_app == :tails do @colors_file Application.compile_env(otp_app, :colors_file) @no_merge_classes Application.compile_env(otp_app, :no_merge_classes) || [] @dark_themes dark_themes || Application.compile_env(otp_app, :dark_themes) @themes themes || Application.compile_env(otp_app, :themes) @custom_variants Application.compile_env(otp_app, :variants) || [] else @colors_file Application.compile_env(otp_app, __MODULE__)[:colors_file] @no_merge_classes Application.compile_env(otp_app, __MODULE__)[:no_merge_classes] || [] @dark_themes dark_themes || Application.compile_env(otp_app, __MODULE__)[:dark_themes] @themes themes || Application.compile_env(otp_app, __MODULE__)[:themes] @custom_variants Application.compile_env(otp_app, __MODULE__)[:variants] || [] end @colors (if @colors_file do @external_resource @colors_file custom = @colors_file |> Path.expand() |> File.read!() |> Jason.decode!() Map.merge(Tails.Colors.builtin_colors(), custom) else Tails.Colors.builtin_colors() end) @all_colors Tails.Colors.all_color_classes(@colors) @variants ~w( hover focus focus-within focus-visible active visited target first last only odd even first-of-type last-of-type only-of-type empty disabled enabled checked indeterminate default required valid invalid in-range out-of-range placeholder-shown autofill read-only open before after first-letter first-line marker selection file backdrop placeholder sm md lg xl 2xl dark portrait landscape motion-safe motion-reduce contrast-more contrast-less rtl ltr ) ++ @custom_variants @font_weights ~w(thin extralight light normal medium semibold bold extrabold black) @font_smoothings ~w(antialiased subpixel-antialiased) @font_styles ~w(italic non-italic) @positions ~w(static fixed absolute relative sticky) @display ~w( 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 ) @bg_sizes ~w(auto cover contain) @bg_repeats ~w(repeat no-repeat repeat-x repeat-y repeat-round repeat-space) @bg_positions ~w(bottom center left left-bottom left-top right right-bottom right-top top) @bg_blends ~w(normal multiply screen overlay darken lighten color-dodge color-burn hard-light soft-light difference exclusion hue saturation color luminosity) @bg_origins ~w(border padding content) @bg_attachments ~w(fixed local scroll) @bg_clips ~w(border padding content text) @bg_images ~w(none gradient-to-t gradient-to-tr gradient-to-r gradient-to-br gradient-to-b gradient-to-bl gradient-to-l gradient-to-tl) @outline_styles ~w(none dashed dotted double) @aspect_ratios ~w(auto square video) @text_overflow ~w(truncate text-ellipsis text-clip) @break_values ~w(auto avoid all avoid-page page left right column) @break_inside_values ~w(auto avoid avoid-page avoid-column) @box_decoration_breaks ~w(clone slice) @box_sizes ~w(border content) @floats ~w(left right none) @clears ~w(left right both none) @isolations ~w(isolate isolation-auto) @flex_directions ~w(row row-reverse col col-reverse) @object_fits ~w(contain cover fill none scale-down) @object_positions ~w(bottom center left left-bottom left-top right right-bottom right-top top) @overflows ~w(auto hidden clip visible scroll) @overscrolls ~w(auto contain none) @visibilities ~w(visible invisible collapse) @flex_wraps ~w(wrap wrap-reverse nowrap) @flex_grow_shrinks ~w(1 auto initial none) @flex_specific_grow_shrinks ~w(0) @grid_flows ~w(row col dense row-dense col-dense) @auto_cols_rows ~w(auto min max fr) @justify_contents ~w(start end center between around evenly) @justify_items ~w(start end center stretch) @justify_selfs ~w(auto start end center stretch) @align_contents ~w(center start end between around evenly baseline) @align_items ~w(start end center baseline stretch) @align_selfs ~w(auto start end center stretch baseline) @place_contents ~w(center start end between around evenly baseline stretch) @place_items ~w(start end center baseline stretch) @place_selfs ~w(auto start end center stretch) @font_families ~w(sans serif mono) @font_variant_numerics ~w(normal-nums ordinal slashed-zero lining-nums oldstyle-nums proportional-nums tabular-nums diagonal-fractions stacked-fractions) @trackings ~w(tighter tight normal wide wider widest) @list_style_types ~w(none disc decimal) @list_style_positions ~w(inside outside) @text_alignments ~w(left center right justify start end) @text_decoration_styles ~w(solid double dotted dashed wavy) @text_decorations ~w(underline overline line-through no-underline) @text_transforms ~w(uppercase lowercase capitalize normal-case) @text_overflows ~w(truncate text-ellipses text-clip) @vertical_alignments ~w(baseline top middle bottom text-top text-bottom sub super) @whitespaces ~w(normal nowrap pre pre-line pre-wrap) @word_breaks ~w(normal words all keep) @border_styles ~w(solid dashed dotted double hidden none) @divide_styles ~w(solid dashed dotted double none) @outline_styles ~w(none dashed dotted double) @blend_modes ~w( normal multiply screen overlay darken lighten color-dodge color-burn hard-light soft-light difference exclusion hue saturation color luminosity ) @table_layouts ~w(auto fixed) @mix_blend_modes @blend_modes ++ ~w(plus-lighter) @bg_blend_modes @blend_modes @border_collapse_modes ~w(collapse separate) @transition_timing_functions ~w(ease-linear ease-in ease-out ease-in-out) @transition_properties ~w(none all colors opacity shadow transform) @animations ~w(none spin ping pulse bounce) @transforms ~w(gpu none) @transform_origins ~w(center top top-right right bottom-right bottom bottom-left left top-left) @cursors ~w( 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 ) @pointer_events ~w(auto none) @resizes ~w(none y x) @scroll_behaviors ~w(auto smooth) @snap_aligns ~w(start end center align-none) @snap_stops ~w(normal always) @snap_types ~w(none x y both) @snap_strictness ~w(mandatory proximity) @touch_actions ~w(auto none pan-x pan-left pan-right pan-y pan-up pan-down pinch-zoom manipulation) @user_selects ~w(none text all auto) @will_change ~w(auto scroll contents transform) @prefixed_with_values [ will_change: %{prefix: "will-change", values: @will_change}, user_select: %{prefix: "select", values: @user_selects}, touch_action: %{prefix: "touch", values: @touch_actions}, snap_type: %{prefix: "snap", values: @snap_types}, snap_strictness: %{prefix: "snap", values: @snap_strictness}, snap_align: %{prefix: "snap", values: @snap_aligns}, snap_stop: %{prefix: "snap", values: @snap_stops}, scroll_behaviour: %{prefix: "scroll", values: @scroll_behaviors}, resize: %{prefix: "resize", values: @resizes, naked?: true}, pointer_events: %{prefix: "pointer-events", values: @pointer_events}, cursor: %{prefix: "cursor", values: @cursors}, transform: %{prefix: "transform", values: @transforms}, animate: %{prefix: "animate", values: @animations}, transition_property: %{prefix: "transition", values: @transition_properties, naked?: true}, table_layout: %{prefix: "table", values: @table_layouts}, border_collapse_mode: %{prefix: "border", values: @border_collapse_modes}, mix_blend_mode: %{prefix: "mix-blend", values: @mix_blend_modes}, bg_blend_mode: %{prefix: "bg-blend", values: @bg_blend_modes}, outline_styles: %{prefix: "outline", values: @outline_styles, naked?: true}, divide_styles: %{prefix: "divide", values: @divide_styles}, border_style: %{prefix: "border", values: @border_styles}, word_breaks: %{prefix: "break", values: @word_breaks}, whitespace: %{prefix: "whitespace", values: @whitespaces}, text_align: %{prefix: "text", values: @text_alignments}, vertical_align: %{prefix: "align", values: @vertical_alignments}, text_decoration_style: %{prefix: "decoration", values: @text_decoration_styles}, list_style_type: %{prefix: "list", values: @list_style_types}, list_style_position: %{prefix: "list", values: @list_style_positions}, tracking: %{prefix: "tracking", values: @trackings}, place_content: %{prefix: "place-content", values: @place_contents}, place_items: %{prefix: "place-items", values: @place_items}, place_selfs: %{prefix: "place-selfs", values: @place_selfs}, align_content: %{prefix: "content", values: @align_contents}, align_items: %{prefix: "items", values: @align_items}, align_selfs: %{prefix: "selfs", values: @align_selfs}, auto_cols: %{prefix: "auto-cols", values: @auto_cols_rows}, auto_rows: %{prefix: "auto-rows", values: @auto_cols_rows}, grid_flow: %{prefix: "grid-flow", values: @grid_flows}, justify_contents: %{prefix: "justify", values: @justify_contents}, justify_items: %{prefix: "justify-items", values: @justify_items}, justify_selfs: %{prefix: "justify-selfs", values: @justify_selfs}, flex_grow_shrink: %{prefix: "flex", values: @flex_grow_shrinks}, flex_direction: %{prefix: "flex", values: @flex_directions}, shrink: %{prefix: "shrink", values: @flex_specific_grow_shrinks, naked?: true}, grow: %{prefix: "grow", values: @flex_specific_grow_shrinks, naked?: true}, flex_wrap: %{prefix: "flex", values: @flex_wraps}, overflow: %{ prefix: "overflow", values: @overflows, overwrites: ~w(overflow_x overflow_y)a }, overflow_x: %{prefix: "overflow-x", values: @overflows}, overflow_y: %{prefix: "overflow-y", values: @overflows}, overscroll: %{ prefix: "overscroll", values: @overscrolls, overwrites: ~w(overscroll_x overscroll_y)a }, overscroll_x: %{prefix: "overscroll-x", values: @overscrolls}, overscroll_y: %{prefix: "overscroll-y", values: @overscrolls}, object_fit: %{prefix: "object", values: @object_fits}, object_position: %{prefix: "object", values: @object_positions}, float: %{prefix: "float", values: @floats}, clear: %{prefix: "clear", values: @clears}, break_after: %{prefix: "break-after", values: @break_values}, break_before: %{prefix: "break-before", values: @break_values}, break_inside: %{prefix: "break-inside", values: @break_inside_values}, box_decoration: %{prefix: "box-decoration", values: @box_decoration_breaks}, box_size: %{prefix: "box", values: @box_sizes}, font_family: %{prefix: "font", values: @font_families}, font_weight: %{prefix: "font", values: @font_weights}, aspect_ratio: %{prefix: "aspect", values: @aspect_ratios}, outline_style: %{prefix: "outline", values: @outline_styles, naked?: true}, bg_size: %{prefix: "bg", values: @bg_sizes}, bg_repeat: %{prefix: "bg", values: @bg_repeats}, bg_positions: %{prefix: "bg", values: @bg_positions}, bg_blend: %{prefix: "bg-blend", values: @bg_blends}, bg_origin: %{prefix: "bg-origin", values: @bg_origins}, bg_clip: %{prefix: "bg-clip", values: @bg_clips}, bg_image: %{prefix: "bg", values: @bg_images}, bg_attachment: %{prefix: "bg", values: @bg_attachments}, col: %{prefix: "col", values: ~w(auto)} ] @prefixed_with_colors [ fill_color: %{prefix: "fill"}, outline_color: %{prefix: "outline"}, caret_color: %{prefix: "caret"}, accent_color: %{prefix: "accent"}, ring_color: %{prefix: "ring"}, shadow_color: %{prefix: "shadow"}, ring_offset_color: %{prefix: "ring-offset"}, divide_color: %{prefix: "divide"}, border_color: %{prefix: "border", overwrites: ~w(border_color_y border_color_x)a}, border_color_y: %{prefix: "border-y"}, border_color_x: %{prefix: "border-x"}, border_color_t: %{prefix: "border-t"}, border_color_r: %{prefix: "border-r"}, border_color_b: %{prefix: "border-b"}, border_color_l: %{prefix: "border-l"}, text_decoration_color: %{prefix: "decoration"}, bg: %{prefix: "bg"}, text_color: %{prefix: "text"} ] @with_values [ sr_only: %{values: ~w(sr-only not-sr-only)}, transition_timing_function: %{ values: @transition_timing_functions, arbitrary_prefix: "ease-" }, text_overflows: %{values: @text_overflows}, text_transform: %{values: @text_transforms}, text_decoration: %{values: @text_decorations}, isolation: %{values: @isolations}, position: %{values: @positions}, font_smoothing: %{values: @font_smoothings}, font_style: %{values: @font_styles}, display: %{values: @display}, visibility: %{values: @visibilities}, text_overflow: %{values: @text_overflow} ] @singletons ~w(container content-none space-y-reverse space-x-reverse divide-y-reverse divide-x-reverse ring-inset filter-none backdrop-filter-none col-auto row-auto) |> Enum.concat(@font_variant_numerics) |> Enum.map(&String.to_atom/1) @prefixed [ stroke_width: %{prefix: "stroke"}, rotate: %{prefix: "rotate"}, translate: %{prefix: "translate"}, translate_x: %{prefix: "translate-x"}, translate_y: %{prefix: "translate-y"}, scale_x: %{prefix: "scale-x"}, scale_y: %{prefix: "scale-y"}, scale: %{prefix: "scale"}, skew_x: %{prefix: "skew-x"}, skew_y: %{prefix: "skew-y"}, duration: %{prefix: "duration"}, delay: %{prefix: "delay"}, blur: %{prefix: "blur", naked?: true}, brightness: %{prefix: "brightness"}, contrast: %{prefix: "contrast"}, sepia: %{prefix: "sepia"}, hue_rotate: %{prefix: "hue-rotate"}, grayscale: %{prefix: "grayscale", naked?: true}, saturate: %{prefix: "saturate"}, invert: %{prefix: "invert", naked?: true}, drop_shadow: %{prefix: "drop-shadow", naked?: true}, shadow: %{prefix: "shadow", naked?: true}, backdrop_blur: %{prefix: "backgdrop-blur", naked?: true}, backdrop_brightness: %{prefix: "backgdrop-brightness"}, backdrop_contrast: %{prefix: "backgdrop-contrast"}, backdrop_sepia: %{prefix: "backgdrop-sepia"}, backdrop_hue_rotate: %{prefix: "backgdrop-hue-rotate"}, backdrop_grayscale: %{prefix: "backgdrop-grayscale", naked?: true}, backdrop_saturate: %{prefix: "backgdrop-saturate"}, backdrop_invert: %{prefix: "backgdrop-invert", naked?: true}, backdrop_opacity: %{prefix: "backdrop-opacity"}, ring_width: %{prefix: "ring", naked?: true}, rounded_t: %{prefix: "rounded-t", naked?: true, overwrites: ~w(rounded_tl rounded_tr)a}, rounded_r: %{prefix: "rounded-r", naked?: true, overwrites: ~w(rounded_tr rounded_br)a}, rounded_b: %{prefix: "rounded-b", naked?: true, overwrites: ~w(rounded_bl rounded_br)a}, rounded_l: %{prefix: "rounded-l", naked?: true, overwrites: ~w(rounded_tl rounded_bl)a}, rounded_tl: %{prefix: "rounded-tl", naked?: true}, rounded_tr: %{prefix: "rounded-tr", naked?: true}, rounded_bl: %{prefix: "rounded-bl", naked?: true}, rounded_br: %{prefix: "rounded-br", naked?: true}, rounded: %{prefix: "rounded", naked?: true, overwrites: ~w( rounded_t rounded_r rounded_b rounded_l rounded_tl rounded_tr rounded_bl rounded_br )a}, underline_offset: %{prefix: "underline-offset"}, text_decoration_thickness: %{prefix: "decoration"}, text_indent: %{prefix: "indent"}, leading: %{prefix: "leading"}, gap: %{prefix: "gap", overwrites: ~w(gap_x gap_y)a}, gap_x: %{prefix: "gap-x"}, gap_y: %{prefix: "gap-y"}, order: %{prefix: "order"}, basis: %{prefix: "basis"}, space_x: %{prefix: "space-x"}, space_y: %{prefix: "space-y"}, z: %{prefix: "z"}, left: %{prefix: "left"}, right: %{prefix: "right"}, top: %{prefix: "top"}, bottom: %{prefix: "bottom"}, inset: %{prefix: "inset", overwrites: ~w(inset_x inset_y top right bottom left)a}, inset_y: %{prefix: "inset-y", overwrites: ~w(top bottom)a}, inset_x: %{prefix: "inset-x", overwrites: ~w(right left)a}, columns: %{prefix: "columns"}, col_span: %{prefix: "col-span"}, col_start: %{prefix: "col-start"}, col_end: %{prefix: "col-end"}, row_span: %{prefix: "row-span"}, row_start: %{prefix: "row-start"}, row_end: %{prefix: "row-end"}, font_size: %{prefix: "text"}, outline_width: %{prefix: "outline"}, outline_offset: %{prefix: "outline-offset"}, grid_cols: %{prefix: "grid-cols"}, grid_rows: %{prefix: "grid-rows"}, width: %{prefix: "w"}, min_width: %{prefix: "min-w"}, max_width: %{prefix: "max-w"}, height: %{prefix: "h"}, min_height: %{prefix: "min-h"}, max_height: %{prefix: "max-h"} ] @simple_overwrite_rules %{ "col-auto" => ~w(col_span col_start col_end)a, "row-auto" => ~w(row_span col_start col_end)a } @directional [ p: %{prefix: "p"}, m: %{prefix: "m"}, scroll_m: %{prefix: "scroll-m"}, scroll_p: %{prefix: "scroll-p"}, divide: %{prefix: "divide", dash_suffix?: true}, border_width: %{prefix: "border", dash_suffix?: true}, border_spacing: %{prefix: "border-spacing", dash_suffix?: true} ] @moduledoc """ Tailwind class utilities like class merging. In most cases, only classes that we know all of the values for are merged. In some cases, we merge anything starting with a given prefix. Eventually, we will have tools to read your tailwind config to determine additional classes to merge, or have explicit configuration on additional merge logic. The preferred way to use tails classes the `classes/1` function. This gives you: 1. conditional classes 2. list merging (from left to right) 3. arbitrary nesting For example: `classes(["mt-1 mx-2", ["pt-2": var1, "pb-4", var2], "mt-12": var3])` Will merge all classes from left to right, flattening the lists and conditionally including the classes where the associated value is true. ## What classes are currently merged? ### Directional classes These classes support x/y/l/r suffix merging. - Padding - `p` - Margin - `m` ### Only explicitly known values Only explicitly known values will be merged, all others will be retained. #{Tails.Doc.doc_with_values(@with_values)} #{Tails.Doc.doc_prefix_with_values(@prefixed_with_values)} #{Tails.Doc.doc_prefixed_with_colors(@prefixed_with_colors)} ### Any values matching prefix Any values matching the following prefixes will be merged with each other respectively #{Tails.Doc.doc_prefixed(@prefixed)} ### Singletons The following classes are tracked as special classes, but don't have any special merge behavior because they either compose with other similar classes or have no conflicts #{Enum.map_join(@singletons, "\n", &"* #{&1}")} """ defstruct Keyword.keys(@directional) ++ Keyword.keys(@prefixed) ++ Keyword.keys(@with_values) ++ Keyword.keys(@prefixed_with_colors) ++ Keyword.keys(@prefixed_with_values) ++ @singletons ++ [ classes: MapSet.new(), variants: %{}, variant: nil, fallback: :default, theme: :default ] defmodule Directions do @moduledoc false defstruct [:l, :r, :t, :b, :tl, :tr, :br, :bl, :x, :y, :all] @type t :: %__MODULE__{ l: String.t(), r: String.t(), t: String.t(), b: String.t(), tl: String.t(), tr: String.t(), br: String.t(), bl: String.t(), x: String.t(), y: String.t() } end @type t :: %__MODULE__{} @doc """ Builds a class string out of a mixed list of inputs or a string. If the value is a string, we make a new `Tails` with it (essentially deduplicating it). If the value is a list, then for each item in the list: - If the value is a list, we call `classes/1` on it. - If it is a tuple, we discard it unless the second element is truthy. - Otherwise, we `to_string` it And then we merge the whole list up into one class string. This allows for conditional class rendering, arbitrarily nested. For example: iex> classes(["a", "b"]) "a b" iex> classes([a: false, b: true]) "b" iex> classes([[a: true, b: false], [c: false, d: true]]) "a d" """ def classes(nil), do: nil def classes(classes) when is_list(classes) do classes |> flatten_classes() |> Enum.reduce("", fn class, acc -> merge(acc, class) end) |> to_string() end def classes(classes) when is_binary(classes) do classes |> new() |> to_string() end def new(classes) do merge(%__MODULE__{}, classes) end defp flatten_classes(classes) do classes |> Enum.filter(fn {_classes, condition} -> condition _ -> true end) |> Enum.map(fn {classes, _} -> classes classes -> classes end) |> Enum.flat_map(fn classes -> if is_list(classes) do flatten_classes(classes) else List.wrap(classes) end end) |> Enum.map(&to_string/1) end @doc """ Semantically merges two lists of tailwind classes, treating the first as a base and the second as overrides Generally, instead of calling `merge/2` you will call `classes/1` with a list of classes. See the module documentation for what classes will be merged/retained. Examples iex> merge("p-4", "p-2") |> to_string() "p-2" iex> merge("p-2", "p-4") |> to_string() "p-4" iex> merge("p-4", "px-2") |> to_string() "p-4 px-2" iex> merge("font-bold", "font-thin") |> to_string() "font-thin" iex> merge("block absolute", "fixed hidden") |> to_string() "fixed hidden" iex> merge("bg-blue-500", "bg-auto") |> to_string() "bg-blue-500 bg-auto" iex> merge("bg-auto", "bg-repeat-x") |> to_string() "bg-auto bg-repeat-x" iex> merge("bg-blue-500", "bg-red-400") |> to_string() "bg-red-400" iex> merge("grid grid-cols-2 lg:grid-cols-3", "grid-cols-3 lg:grid-cols-4") |> to_string() "grid grid-cols-3 lg:grid-cols-4" iex> merge("font-normal text-black hover:text-blue-300", "text-gray-600 dark:text-red-400 font-bold") |> to_string() "text-gray-600 font-bold hover:text-blue-300 dark:text-red-400" iex> merge("min-h-2", "min-h-[1rem]") |> to_string() "min-h-[1rem]" iex> merge("min-w-2", "min-h-[1rem]") |> to_string() "min-w-2 min-h-[1rem]" iex> merge("border-2", "border-gray-500") |> to_string() "border-2 border-gray-500" iex> merge("rounded-lg", "rounded") |> to_string() "rounded" iex> merge("rounded", "rounded-lg") |> to_string() "rounded-lg" iex> merge("border-separate", "border-spacing-1") |> to_string() "border-spacing-1 border-separate" iex> merge("shadow", "shadow-md") |> to_string() "shadow-md" iex> merge("shadow-none", "shadow-inner") |> to_string() "shadow-inner" iex> merge("shadow-lg", "shadow") |> to_string() "shadow" Classes can be removed iex> merge("font-normal text-black", "remove:font-normal grid") |> to_string() "grid text-black" All preceding classes can be removed iex> merge("font-normal text-black", "remove:* grid") |> to_string() "grid" Classes can be explicitly kept iex> merge("font-normal text-black", "remove:font-normal grid") |> to_string() "grid text-black" """ def merge(tailwind, nil) when is_binary(tailwind), do: new(tailwind) def merge(%__MODULE__{} = tailwind, nil), do: new(tailwind) def merge(tailwind, classes) when is_list(tailwind) do merge(classes(tailwind), classes) end def merge(tailwind, classes) when is_list(classes) do merge(tailwind, classes(classes)) end def merge(tailwind, classes) when is_binary(tailwind) do merge(new(tailwind), classes) end def merge(tailwind, classes) when is_binary(classes) do classes |> String.split() |> Enum.reduce(tailwind, &merge_class(&2, &1)) end def merge(tailwind, %__MODULE__{} = classes) do merge(tailwind, to_string(classes)) end def merge(_tailwind, value) do raise "Cannot merge #{inspect(value)}" end @doc "Merges a list of class strings. See `merge/2` for more" def merge(list) when is_list(list) do Enum.reduce(list, &merge(&2, &1)) end def merge(value) when not is_list(value) do merge([value]) end defp set_theme(tailwind, theme) do %{ tailwind | theme: theme, variants: Map.new(tailwind.variants, fn {key, value} -> {key, set_theme(value, theme)} end) } end defp alter_dark_theme(%{variant: "dark"} = tailwind, fallback, theme) do tailwind |> set_theme(theme) |> set_fallback_theme(fallback) end defp alter_dark_theme(tailwind, fallback, theme) do %{ tailwind | variants: Map.new(tailwind.variants, fn {key, value} -> {key, alter_dark_theme(value, fallback, theme)} end) } end defp set_fallback_theme(tailwind, fallback) do %{ tailwind | fallback: fallback, variants: Map.new(tailwind.variants, fn {key, value} -> {key, set_fallback_theme(value, fallback)} end) } end for {source_class, overwrites} <- @simple_overwrite_rules do for overwrite <- overwrites do def merge_class(%{unquote(overwrite) => v} = tailwind, unquote(to_string(source_class))) when not is_nil(v) do tailwind |> Map.put(unquote(overwrite), nil) |> merge_class(unquote(to_string(source_class))) end end end @doc false def merge_class(tailwind, "keep:" <> class) do %{tailwind | classes: MapSet.put(tailwind.classes, class)} end def merge_class(%{theme: theme, variants: variants}, "remove:*") do %__MODULE__{ theme: theme, variants: Map.new(variants, fn {key, value} -> {key, merge_class(value, "remove:*")} end) } end def merge_class(tailwind, "remove:" <> class) do remove(tailwind, class) end replacements = Enum.flat_map(@themes || [], fn {theme, replacements} -> replacements |> Tails.Custom.replacements() |> Enum.map(fn {key, replacement} -> {theme, key, replacement} end) end) for {theme, _replacements} <- @themes || [] do if @dark_themes[theme] do def merge_class(tailwind, "theme:" <> unquote(to_string(theme))) do tailwind |> set_theme(unquote(theme)) |> alter_dark_theme(unquote(theme), unquote(@dark_themes[theme])) end else def merge_class(tailwind, "theme:" <> unquote(to_string(theme))) do set_theme(tailwind, unquote(theme)) end end end # First match on exact theme matches for {theme, key, replacement} <- replacements do dark_replacement = Enum.find_value(replacements, fn {r_theme, r_key, dark_replacement} -> if r_theme == @dark_themes[theme] && r_key == key do dark_replacement end end) if dark_replacement do def merge_class(%{theme: theme} = tailwind, unquote(to_string(key))) when theme in [unquote(theme), unquote(to_string(theme))] do merge(tailwind, classes([unquote(replacement), unquote("dark:#{dark_replacement}")])) end else def merge_class(%{theme: theme} = tailwind, unquote(to_string(key))) when theme in [unquote(theme), unquote(to_string(theme))] do merge(tailwind, classes(unquote(replacement))) end end end # # Then match on each theme as a potential fallback theme # # We don't check for a matching dark theme when falling back for {theme, key, replacement} <- replacements do def merge_class(%{fallback: theme} = tailwind, unquote(to_string(key))) when theme in [unquote(theme), unquote(to_string(theme))] do merge(tailwind, classes(unquote(replacement))) end end # final fallback to default for {:default, key, replacement} <- replacements do dark_replacement = Enum.find_value(replacements, fn {r_theme, r_key, dark_replacement} -> if r_theme == @dark_themes[:default] && r_key == key do dark_replacement end end) if dark_replacement do def merge_class(tailwind, unquote(to_string(key))) do merge( tailwind, classes([unquote(replacement), unquote("dark:#{dark_replacement}")]) ) end else def merge_class(tailwind, unquote(to_string(key))) do merge(tailwind, classes(unquote(replacement))) end end end for class <- @no_merge_classes || [] do def merge_class(tailwind, unquote(class)) do %{tailwind | classes: MapSet.put(tailwind.classes, class)} end end for modifier <- @variants do def merge_class(tailwind, unquote(modifier) <> ":" <> rest) do rest = String.split(rest, ":") last = List.last(rest) variants = :lists.droplast(rest) key = Enum.sort([unquote(modifier) | variants]) if Map.has_key?(tailwind.variants, key) do %{tailwind | variants: Map.update!(tailwind.variants, key, &merge_class(&1, last))} else %{ tailwind | variants: Map.put(tailwind.variants, key, %{new(last) | variant: Enum.join(key, ":")}) } end end end for value <- @singletons do def merge_class(tailwind, unquote(to_string(value))) do Map.put(tailwind, unquote(value), true) end end for {key, %{values: values, prefix: prefix} = config} <- Enum.sort_by(@prefixed_with_values, fn {_, %{prefix: prefix}} -> -String.length(prefix) end) do if config[:naked?] do def merge_class(tailwind, unquote(prefix)) do Map.put(tailwind, unquote(key), "") end end unless config[:no_arbitrary?] do if config[:arbitrary_prefix] do def merge_class( tailwind, unquote(config[:arbitrary_prefix]) <> "-" <> "[" <> new_value ) do Map.put(tailwind, unquote(key), "[" <> new_value) end else def merge_class(tailwind, unquote(prefix) <> "-" <> "[" <> new_value) do Map.put(tailwind, unquote(key), "[" <> new_value) end end end def merge_class(tailwind, unquote(prefix) <> "-" <> new_value) when new_value in unquote(values) do Map.put(tailwind, unquote(key), new_value) end end for {key, %{prefix: prefix} = config} <- Enum.sort_by(@prefixed_with_colors, fn {_, %{prefix: prefix}} -> -String.length(prefix) end) do def merge_class(tailwind, unquote(prefix) <> "-" <> "[" <> new_value) do Map.put(tailwind, unquote(key), "[" <> new_value) end def merge_class(tailwind, unquote(prefix) <> "-" <> new_value) when new_value in @all_colors do Map.put(tailwind, unquote(key), new_value) end end for {key, %{values: values}} <- @with_values do def merge_class(tailwind, new_value) when new_value in unquote(values) do Map.put(tailwind, unquote(key), new_value) end end for {class, %{prefix: string_class}} <- @directional do @dirs %{ x: [:r, :l], y: [:t, :b], t: [:tl, :tr], r: [:tr, :br], b: [:br, :bl], l: [:tl, :bl] } for {dir, clears} <- @dirs do @clears Enum.map(clears, &{&1, nil}) def merge_class( %{unquote(class) => nil} = tailwind, unquote(string_class) <> unquote(to_string(dir)) <> "-" <> value ) do Map.put(tailwind, unquote(class), struct(Directions, %{unquote(dir) => value})) end def merge_class( %{unquote(class) => nil} = tailwind, "-" <> unquote(string_class) <> unquote(to_string(dir)) <> "-" <> value ) do Map.put(tailwind, unquote(class), struct(Directions, %{unquote(dir) => "-" <> value})) end def merge_class( %{unquote(class) => directions} = tailwind, unquote(string_class) <> unquote(to_string(dir)) <> "-" <> value ) do Map.put( tailwind, unquote(class), struct(directions, [{unquote(dir), value} | @clears]) ) end def merge_class( %{unquote(class) => directions} = tailwind, "-" <> unquote(string_class) <> unquote(to_string(dir)) <> "-" <> value ) do Map.put( tailwind, unquote(class), struct(directions, [{unquote(dir), "-" <> value} | @clears]) ) end def merge_class(tailwind, "-" <> unquote(string_class) <> "-" <> value) do Map.put(tailwind, unquote(class), %Directions{all: "-#{value}"}) end def merge_class(tailwind, unquote(string_class) <> "-" <> value) do Map.put(tailwind, unquote(class), %Directions{all: value}) end end end for {key, %{prefix: prefix} = config} <- Enum.sort_by(@prefixed, fn {_, %{prefix: prefix}} -> -String.length(prefix) end) do if config[:naked?] do def merge_class(tailwind, unquote(prefix)) do Map.put(tailwind, unquote(key), "") end end unless config[:no_arbitrary?] do def merge_class(tailwind, unquote(prefix) <> "-" <> "[" <> _ = value_without_suffix) do unquote(prefix) <> "-" <> new_value = value_without_suffix Map.put(tailwind, unquote(key), new_value) end end def merge_class(tailwind, unquote(prefix) <> "-" <> new_value) do Map.put(tailwind, unquote(key), new_value) end end def merge_class(tailwind, class) do %{tailwind | classes: MapSet.put(tailwind.classes, class)} end @doc """ Removes the given class from the class list. """ for class <- @no_merge_classes || [] do def remove(tailwind, unquote(class)) do %{tailwind | classes: MapSet.delete(tailwind.classes, class)} end end for modifier <- @variants do def remove(tailwind, unquote(modifier) <> ":" <> rest) do rest = String.split(rest, ":") last = List.last(rest) variants = :lists.droplast(rest) key = Enum.sort([unquote(modifier) | variants]) if Map.has_key?(tailwind.variants, key) do %{tailwind | variants: Map.update!(tailwind.variants, key, &remove(&1, last))} else %{ tailwind | variants: Map.put(tailwind.variants, key, %{new(last) | variant: Enum.join(key, ":")}) } end end end def remove(tails, class) do if MapSet.member?(tails.classes, class) do %{tails | classes: MapSet.delete(tails.classes, class)} else # This is a bit of a hack, could maybe be optimized or fixed later # explicitly kept classes using `keep:` are in `classes`, so we keep them separate. The rest are classes we don't know about # so won't be affected by the `new/1` call below anyway. keep = tails.classes tails |> Map.put(:classes, MapSet.new()) |> to_string() |> String.split(" ") |> Kernel.--([class]) |> Enum.join(" ") |> new() |> Map.put(:classes, keep) end end @doc false def to_iodata(tailwind) do [ Enum.map(@with_values, fn {key, _} -> simple(Map.get(tailwind, key), tailwind.variant) end), Enum.map(@directional, fn {key, %{prefix: prefix} = config} -> directional(Map.get(tailwind, key), prefix, !!config[:dash_suffix?], tailwind.variant) end), @singletons |> Enum.filter(&Map.get(tailwind, &1)) |> Enum.map(fn class -> simple(to_string(class), tailwind.variant) end), Enum.map(@prefixed_with_colors, fn {key, %{prefix: prefix} = config} -> prefix(prefix, Map.get(tailwind, key), tailwind.variant, false) end), Enum.map(@prefixed_with_values, fn {key, %{prefix: prefix} = config} -> prefix(prefix, Map.get(tailwind, key), tailwind.variant, config[:naked?]) end), Enum.map(@prefixed, fn {key, %{prefix: prefix} = config} -> prefix(prefix, Map.get(tailwind, key), tailwind.variant, config[:naked?]) end) ] |> add_variants(tailwind) |> add_classes(tailwind, tailwind.variant) end defp add_variants(iodata, tailwind) do [ iodata, tailwind.variants |> Kernel.||(%{}) |> Enum.sort_by(&elem(&1, 1)) |> Enum.map(fn {_key, value} -> to_iodata(value) end) ] end defp add_classes(iodata, tailwind, variant) do Enum.concat( iodata, case tailwind.classes do nil -> [] "" -> [] classes -> if Enum.empty?(classes) do [] else if variant do [" " | Enum.intersperse(Enum.map(tailwind.classes, &[variant, ":", &1]), " ")] else [" " | Enum.intersperse(tailwind.classes, " ")] end end end ) end defp simple(nil, _), do: "" defp simple(value, nil), do: [" ", value] defp simple(value, variant), do: [" ", variant, ":", value] defp prefix(prefix, value, variant, naked? \\ false) defp prefix(_prefix, nil, _, _), do: "" defp prefix(prefix, empty, nil, true) when empty in ["", nil], do: [" ", prefix] defp prefix(prefix, value, nil, _), do: [" ", prefix, "-", value] defp prefix(prefix, empty, variant, true) when empty in ["", nil], do: [" ", variant, ":", prefix] defp prefix(prefix, value, variant, _), do: [" ", variant, ":", prefix, "-", value] defp directional(nil, _key, _, _), do: "" defp directional( %Directions{ l: l, r: r, t: t, b: b, x: x, y: y, tl: tl, tr: tr, bl: bl, br: br, all: all }, key, dash_suffix?, variant ) do [ direction(all, nil, key, variant, dash_suffix?), direction(tl, "tl", key, variant, dash_suffix?), direction(tr, "tr", key, variant, dash_suffix?), direction(bl, "bl", key, variant, dash_suffix?), direction(br, "br", key, variant, dash_suffix?), direction(t, "t", key, variant, dash_suffix?), direction(b, "b", key, variant, dash_suffix?), direction(l, "l", key, variant, dash_suffix?), direction(r, "r", key, variant, dash_suffix?), direction(x, "x", key, variant, dash_suffix?), direction(y, "y", key, variant, dash_suffix?) ] |> Enum.filter(& &1) end defp direction(nil, _, _, _, _), do: "" defp direction("-" <> value, suffix, prefix, nil, dash_suffix?), do: [" -", prefix, dash_suffix(suffix, dash_suffix?), "-", value] defp direction(value, suffix, prefix, nil, dash_suffix?), do: [" ", prefix, dash_suffix(suffix, dash_suffix?), "-", value] defp direction("-" <> value, suffix, prefix, variant, dash_suffix?), do: [" ", variant, ":-", prefix, dash_suffix(suffix, dash_suffix?), "-", value] defp direction(value, suffix, prefix, variant, dash_suffix?), do: [" ", variant, ":", prefix, dash_suffix(suffix, dash_suffix?), "-", value] defp dash_suffix(value, true) when not is_nil(value), do: ["-", value] defp dash_suffix(nil, _), do: "" defp dash_suffix(value, _), do: value defimpl String.Chars do def to_string(tailwind) do tailwind |> to_iodata() |> IO.iodata_to_binary() |> case do " " <> rest -> rest value -> value end end defp to_iodata(%mod{} = tailwind) do mod.to_iodata(tailwind) end end defimpl Inspect do def inspect(tailwind, _opts) do "Tails.classes(\"#{to_string(tailwind)}\")" end end for {key, value} when is_binary(value) <- @colors do # sobelow_skip ["DOS.BinToAtom"] def unquote(:"#{String.replace(key, "-", "_")}")() do unquote(value) end end # for {key, value} when is_map(value) <- @colors do # for {suffix, value} when is_binary(value) <- value do # if suffix == "DEFAULT" do # # sobelow_skip ["DOS.BinToAtom"] # def unquote(:"#{String.replace(key, "-", "_")}")() do # unquote(value) # end # else # # sobelow_skip ["DOS.BinToAtom"] # def unquote(:"#{String.replace(key, "-", "_")}_#{String.replace(suffix, "-", "_")}")() do # unquote(value) # end # end # end # end end end @doc false def replacements(replacements, prefix \\ "") do Enum.flat_map(replacements, fn {key, value} when is_binary(value) -> [{add_to_prefix(prefix, key), value}] {key, value} when is_list(value) -> replacements(value, add_to_prefix(prefix, key)) end) end defp add_to_prefix("", value), do: to_string(value) defp add_to_prefix(prefix, value), do: to_string(prefix) <> "-" <> to_string(value) end