remove Tailwind + DaisyUI theme + heroicons plugin, admin fully custom CSS (Phase 7)
replace Tailwind CLI with esbuild for admin CSS bundling. admin now uses hand-written utility classes (admin/utilities.css), static heroicon CSS generated by mix generate_admin_icons, plain CSS colour themes extracted from DaisyUI plugin config, and minimal resets. rename app.css to admin.css for clarity alongside shop.css. delete vendor/daisyui-theme.js and vendor/heroicons.js. no Tailwind dependency remains in the project. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
126
lib/mix/tasks/generate_admin_icons.ex
Normal file
126
lib/mix/tasks/generate_admin_icons.ex
Normal file
@@ -0,0 +1,126 @@
|
||||
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
|
||||
@@ -133,10 +133,9 @@ defmodule Mix.Tasks.Lighthouse do
|
||||
defp build_prod_assets! do
|
||||
Mix.shell().info("Building production assets...")
|
||||
|
||||
Mix.Task.run("tailwind", ["simpleshop_theme", "--minify"])
|
||||
Mix.Task.run("tailwind", ["simpleshop_theme_shop", "--minify"])
|
||||
Mix.Task.run("esbuild", ["simpleshop_theme", "--minify"])
|
||||
Mix.Task.run("esbuild", ["simpleshop_theme_shop_css", "--minify"])
|
||||
Mix.Task.run("esbuild", ["simpleshop_theme_admin_css", "--minify"])
|
||||
Mix.Task.run("phx.digest")
|
||||
|
||||
Mix.shell().info(" Assets built and digested.")
|
||||
|
||||
@@ -158,9 +158,9 @@ defmodule Mix.Tasks.Screenshots do
|
||||
defp build_prod_assets! do
|
||||
Mix.shell().info("Building production assets...")
|
||||
|
||||
Mix.Task.run("tailwind", ["simpleshop_theme", "--minify"])
|
||||
Mix.Task.run("esbuild", ["simpleshop_theme", "--minify"])
|
||||
Mix.Task.run("esbuild", ["simpleshop_theme_shop_css", "--minify"])
|
||||
Mix.Task.run("esbuild", ["simpleshop_theme_admin_css", "--minify"])
|
||||
Mix.Task.run("phx.digest")
|
||||
|
||||
Mix.shell().info(" Assets built and digested.")
|
||||
|
||||
@@ -393,8 +393,8 @@ defmodule SimpleshopThemeWeb.CoreComponents do
|
||||
You can customize the size and colors of the icons by setting
|
||||
width, height, and background color classes.
|
||||
|
||||
Icons are extracted from the `deps/heroicons` directory and bundled within
|
||||
your compiled app.css by the plugin in `assets/vendor/heroicons.js`.
|
||||
Icons are extracted from the `deps/heroicons` directory and bundled into
|
||||
`admin/icons.css` by `mix generate_admin_icons`.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -98,14 +98,14 @@ defmodule SimpleshopThemeWeb.Layouts do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Provides dark vs light theme toggle based on themes defined in app.css.
|
||||
Provides dark vs light theme toggle based on themes defined in admin.css.
|
||||
|
||||
See <head> in root.html.heex which applies the theme before page load.
|
||||
"""
|
||||
def theme_toggle(assigns) do
|
||||
~H"""
|
||||
<div class="card relative flex flex-row items-center border-2 border-base-300 bg-base-300 rounded-full">
|
||||
<div class="absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0 [[data-theme=light]_&]:left-1/3 [[data-theme=dark]_&]:left-2/3 transition-[left]" />
|
||||
<div class="theme-toggle-indicator absolute w-1/3 h-full rounded-full border-1 border-base-200 bg-base-100 brightness-200 left-0" />
|
||||
|
||||
<button
|
||||
class="flex p-2 cursor-pointer w-1/3"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<style>
|
||||
@layer properties, reset, primitives, tokens, theme, base, components, layout, utilities, overrides;
|
||||
</style>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/admin.css"} />
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/shop.css"} />
|
||||
<script defer phx-track-static src={~p"/assets/js/app.js"}>
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<.live_title default="SimpleshopTheme" suffix=" · Phoenix Framework">
|
||||
{assigns[:page_title]}
|
||||
</.live_title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/admin.css"} />
|
||||
<script defer phx-track-static src={~p"/assets/js/app.js"}>
|
||||
</script>
|
||||
<script>
|
||||
|
||||
@@ -66,7 +66,7 @@ defmodule SimpleshopThemeWeb.ErrorHTML do
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{@error_code} - {@error_title}</title>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/css/admin.css"} />
|
||||
<style id="theme-css">
|
||||
<%= Phoenix.HTML.raw(@generated_css) %>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user