127 lines
3.7 KiB
Elixir
127 lines
3.7 KiB
Elixir
|
|
defmodule Mix.Tasks.GenerateAdminIcons do
|
||
|
|
@moduledoc """
|
||
|
|
Generates static CSS for heroicon classes used in admin templates.
|
||
|
|
|
||
|
|
Scans `lib/` for `hero-*` class references, reads the matching SVGs from
|
||
|
|
`deps/heroicons/optimized/`, and writes `assets/css/admin/icons.css` with
|
||
|
|
CSS mask-based icon classes that match the old Tailwind plugin output.
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
mix generate_admin_icons
|
||
|
|
"""
|
||
|
|
|
||
|
|
@shortdoc "Generate admin/icons.css from heroicons SVGs"
|
||
|
|
|
||
|
|
use Mix.Task
|
||
|
|
|
||
|
|
@icons_dir "deps/heroicons/optimized"
|
||
|
|
@output_path "assets/css/admin/icons.css"
|
||
|
|
|
||
|
|
# Maps suffix to subdirectory and default size
|
||
|
|
@variants %{
|
||
|
|
"" => {"24/outline", "1.5rem"},
|
||
|
|
"-solid" => {"24/solid", "1.5rem"},
|
||
|
|
"-mini" => {"20/solid", "1.25rem"},
|
||
|
|
"-micro" => {"16/solid", "1rem"}
|
||
|
|
}
|
||
|
|
|
||
|
|
@impl Mix.Task
|
||
|
|
def run(_args) do
|
||
|
|
icons = scan_used_icons()
|
||
|
|
Mix.shell().info("Found #{length(icons)} icon references in lib/")
|
||
|
|
|
||
|
|
css_rules =
|
||
|
|
icons
|
||
|
|
|> Enum.sort()
|
||
|
|
|> Enum.map(&generate_rule/1)
|
||
|
|
|> Enum.reject(&is_nil/1)
|
||
|
|
|
||
|
|
missing = length(icons) - length(css_rules)
|
||
|
|
if missing > 0, do: Mix.shell().info("Skipped #{missing} (SVG not found or not an icon)")
|
||
|
|
|
||
|
|
content = """
|
||
|
|
/* Generated by mix generate_admin_icons — do not edit by hand */
|
||
|
|
|
||
|
|
#{Enum.join(css_rules, "\n\n")}
|
||
|
|
"""
|
||
|
|
|
||
|
|
File.mkdir_p!(Path.dirname(@output_path))
|
||
|
|
File.write!(@output_path, content)
|
||
|
|
Mix.shell().info("Wrote #{length(css_rules)} icon rules to #{@output_path}")
|
||
|
|
end
|
||
|
|
|
||
|
|
defp scan_used_icons do
|
||
|
|
Path.wildcard("lib/**/*.{ex,heex}")
|
||
|
|
|> Enum.flat_map(fn path ->
|
||
|
|
path
|
||
|
|
|> File.read!()
|
||
|
|
|> then(&Regex.scan(~r/hero-[a-z][a-z0-9-]+/, &1))
|
||
|
|
|> List.flatten()
|
||
|
|
end)
|
||
|
|
|> Enum.uniq()
|
||
|
|
|> Enum.reject(&shop_class?/1)
|
||
|
|
end
|
||
|
|
|
||
|
|
# Filter out CSS class names from shop components that start with "hero-"
|
||
|
|
# but aren't actual heroicon references
|
||
|
|
defp shop_class?(name) do
|
||
|
|
name in [
|
||
|
|
"hero-section",
|
||
|
|
"hero-description",
|
||
|
|
"hero-cta",
|
||
|
|
"hero-cta-group",
|
||
|
|
"hero-pre-title",
|
||
|
|
"hero-error",
|
||
|
|
"hero-image",
|
||
|
|
"hero-section--page"
|
||
|
|
]
|
||
|
|
end
|
||
|
|
|
||
|
|
defp generate_rule(class_name) do
|
||
|
|
{suffix, {dir, size}} = detect_variant(class_name)
|
||
|
|
base = String.trim_leading(class_name, "hero-")
|
||
|
|
svg_name = if suffix == "", do: base, else: String.trim_trailing(base, suffix)
|
||
|
|
svg_path = Path.join([@icons_dir, dir, "#{svg_name}.svg"])
|
||
|
|
|
||
|
|
if File.exists?(svg_path) do
|
||
|
|
content =
|
||
|
|
svg_path
|
||
|
|
|> File.read!()
|
||
|
|
|> String.replace(~r/\r?\n|\r/, "")
|
||
|
|
|> URI.encode(&uri_allowed?/1)
|
||
|
|
|
||
|
|
"""
|
||
|
|
.#{class_name} {
|
||
|
|
--hero-#{class_name |> String.trim_leading("hero-")}: url('data:image/svg+xml;utf8,#{content}');
|
||
|
|
-webkit-mask: var(--hero-#{class_name |> String.trim_leading("hero-")});
|
||
|
|
mask: var(--hero-#{class_name |> String.trim_leading("hero-")});
|
||
|
|
mask-repeat: no-repeat;
|
||
|
|
background-color: currentColor;
|
||
|
|
vertical-align: middle;
|
||
|
|
display: inline-block;
|
||
|
|
width: #{size};
|
||
|
|
height: #{size};
|
||
|
|
}\
|
||
|
|
"""
|
||
|
|
else
|
||
|
|
Mix.shell().info(" warning: SVG not found for #{class_name} (expected #{svg_path})")
|
||
|
|
nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
defp detect_variant(class_name) do
|
||
|
|
cond do
|
||
|
|
String.ends_with?(class_name, "-micro") -> {"-micro", @variants["-micro"]}
|
||
|
|
String.ends_with?(class_name, "-mini") -> {"-mini", @variants["-mini"]}
|
||
|
|
String.ends_with?(class_name, "-solid") -> {"-solid", @variants["-solid"]}
|
||
|
|
true -> {"", @variants[""]}
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# Characters that don't need percent-encoding in data URIs
|
||
|
|
defp uri_allowed?(char) do
|
||
|
|
URI.char_unreserved?(char) or char in [?<, ?>, ?/, ?=, ?", ?', ?:, ?;, ?(, ?), ?!, ?@, ?,, ?.]
|
||
|
|
end
|
||
|
|
end
|