refactor admin CSS: replace utility classes with semantic styles
Replace Tailwind utility soup across admin templates with semantic CSS classes. Add layout primitives (stack, row, cluster, grid), extract JS transition helpers into transitions.css, and refactor core_components, layouts, settings, newsletter, order_show, providers, and theme editor templates. Utility occurrences reduced from 290+ to 127 across admin files. All 1679 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -276,15 +276,15 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="max-w-2xl">
|
||||
<div class="admin-settings">
|
||||
<.header>
|
||||
Settings
|
||||
</.header>
|
||||
|
||||
<%!-- Shop status --%>
|
||||
<section class="mt-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold">Shop status</h2>
|
||||
<section class="admin-section">
|
||||
<div class="admin-section-header">
|
||||
<h2 class="admin-section-title">Shop status</h2>
|
||||
<%= if @site_live do %>
|
||||
<.status_pill color="green">
|
||||
<.icon name="hero-check-circle-mini" class="size-3" /> Live
|
||||
@@ -293,22 +293,19 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
<.status_pill color="zinc">Offline</.status_pill>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-base-content/60">
|
||||
<p class="admin-section-desc">
|
||||
<%= if @site_live do %>
|
||||
Your shop is visible to the public.
|
||||
<% else %>
|
||||
Your shop is offline. Visitors see a "coming soon" page.
|
||||
<% end %>
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<div class="admin-section-body">
|
||||
<button
|
||||
phx-click="toggle_site_live"
|
||||
class={[
|
||||
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold shadow-xs",
|
||||
if(@site_live,
|
||||
do: "bg-base-200 text-base-content hover:bg-base-300 ring-1 ring-base-300 ring-inset",
|
||||
else: "bg-green-600 text-white hover:bg-green-500"
|
||||
)
|
||||
"admin-btn admin-btn-sm",
|
||||
if(@site_live, do: "admin-btn-outline", else: "admin-btn-primary")
|
||||
]}
|
||||
>
|
||||
<%= if @site_live do %>
|
||||
@@ -321,9 +318,9 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</section>
|
||||
|
||||
<%!-- Payments --%>
|
||||
<section class="mt-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold">Payments</h2>
|
||||
<section class="admin-section">
|
||||
<div class="admin-section-header">
|
||||
<h2 class="admin-section-title">Payments</h2>
|
||||
<%= case @stripe_status do %>
|
||||
<% :connected -> %>
|
||||
<.status_pill color="green">
|
||||
@@ -354,9 +351,9 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</section>
|
||||
|
||||
<%!-- Products --%>
|
||||
<section class="mt-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold">Products</h2>
|
||||
<section class="admin-section">
|
||||
<div class="admin-section-header">
|
||||
<h2 class="admin-section-title">Products</h2>
|
||||
<%= if @provider do %>
|
||||
<.status_pill color="green">
|
||||
<.icon name="hero-check-circle-mini" class="size-3" /> Connected
|
||||
@@ -369,15 +366,12 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
<%= if @provider do %>
|
||||
<.provider_connected provider={@provider} />
|
||||
<% else %>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">
|
||||
<div class="admin-section-body">
|
||||
<p class="admin-section-desc" style="margin-top: 0;">
|
||||
Connect a print-on-demand provider to import products into your shop.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<.link
|
||||
navigate={~p"/admin/providers"}
|
||||
class="inline-flex items-center gap-2 rounded-md bg-base-content px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-base-content/80"
|
||||
>
|
||||
<div class="admin-section-body">
|
||||
<.link navigate={~p"/admin/providers"} class="admin-btn admin-btn-primary admin-btn-sm">
|
||||
<.icon name="hero-plus-mini" class="size-4" /> Connect a provider
|
||||
</.link>
|
||||
</div>
|
||||
@@ -386,9 +380,9 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</section>
|
||||
|
||||
<%!-- Cart recovery --%>
|
||||
<section class="mt-10">
|
||||
<div class="flex items-center gap-3">
|
||||
<h2 class="text-lg font-semibold">Cart recovery</h2>
|
||||
<section class="admin-section">
|
||||
<div class="admin-section-header">
|
||||
<h2 class="admin-section-title">Cart recovery</h2>
|
||||
<%= if @cart_recovery_enabled do %>
|
||||
<.status_pill color="green">
|
||||
<.icon name="hero-check-circle-mini" class="size-3" /> On
|
||||
@@ -397,42 +391,35 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
<.status_pill color="zinc">Off</.status_pill>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-base-content/60">
|
||||
<p class="admin-section-desc">
|
||||
When on, customers who entered their email at Stripe checkout but didn't complete
|
||||
payment receive a single plain-text recovery email one hour later.
|
||||
No tracking pixels. One email, never more.
|
||||
</p>
|
||||
<%= if @cart_recovery_enabled do %>
|
||||
<p class="mt-2 text-sm text-amber-700">
|
||||
<p class="admin-section-desc" style="color: #b45309;">
|
||||
Make sure your privacy policy mentions that a single recovery email may be sent,
|
||||
and that customers can unsubscribe at any time.
|
||||
</p>
|
||||
<% end %>
|
||||
<div class="mt-4">
|
||||
<div class="admin-section-body">
|
||||
<button
|
||||
phx-click="toggle_cart_recovery"
|
||||
class={[
|
||||
"inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-semibold shadow-xs",
|
||||
if(@cart_recovery_enabled,
|
||||
do: "bg-base-200 text-base-content hover:bg-base-300 ring-1 ring-base-300 ring-inset",
|
||||
else: "bg-base-content text-white hover:bg-base-content/80"
|
||||
)
|
||||
"admin-btn admin-btn-sm",
|
||||
if(@cart_recovery_enabled, do: "admin-btn-outline", else: "admin-btn-primary")
|
||||
]}
|
||||
>
|
||||
<%= if @cart_recovery_enabled do %>
|
||||
Turn off
|
||||
<% else %>
|
||||
Turn on
|
||||
<% end %>
|
||||
{if @cart_recovery_enabled, do: "Turn off", else: "Turn on"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Account --%>
|
||||
<section class="mt-10">
|
||||
<h2 class="text-lg font-semibold">Account</h2>
|
||||
<section class="admin-section">
|
||||
<h2 class="admin-section-title">Account</h2>
|
||||
|
||||
<div class="mt-4 space-y-6">
|
||||
<div class="admin-stack" style="margin-top: 1rem; --admin-stack-gap: 1.5rem;">
|
||||
<.form
|
||||
for={@email_form}
|
||||
id="email_form"
|
||||
@@ -446,12 +433,12 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
autocomplete="username"
|
||||
required
|
||||
/>
|
||||
<div class="mt-3">
|
||||
<div style="margin-top: 0.75rem;">
|
||||
<.button phx-disable-with="Saving...">Change email</.button>
|
||||
</div>
|
||||
</.form>
|
||||
|
||||
<div class="border-t border-base-200 pt-6">
|
||||
<div style="border-top: 1px solid var(--t-surface-sunken); padding-top: 1.5rem;">
|
||||
<.form
|
||||
for={@password_form}
|
||||
id="password_form"
|
||||
@@ -481,7 +468,7 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
label="Confirm new password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div class="mt-3">
|
||||
<div style="margin-top: 0.75rem;">
|
||||
<.button phx-disable-with="Saving...">Change password</.button>
|
||||
</div>
|
||||
</.form>
|
||||
@@ -490,17 +477,14 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</section>
|
||||
|
||||
<%!-- Advanced --%>
|
||||
<section class="mt-10 pb-10">
|
||||
<h2 class="text-lg font-semibold">Advanced</h2>
|
||||
<section class="admin-section" style="padding-bottom: 2.5rem;">
|
||||
<h2 class="admin-section-title">Advanced</h2>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<.link
|
||||
href={~p"/admin/dashboard"}
|
||||
class="text-sm text-base-content/60 hover:text-base-content"
|
||||
>
|
||||
<div class="admin-stack" style="margin-top: 1rem; --admin-stack-gap: 0.5rem;">
|
||||
<.link href={~p"/admin/dashboard"} class="admin-link-subtle">
|
||||
<.icon name="hero-chart-bar" class="size-4 inline" /> System dashboard
|
||||
</.link>
|
||||
<.link href={~p"/admin/errors"} class="text-sm text-base-content/60 hover:text-base-content">
|
||||
<.link href={~p"/admin/errors"} class="admin-link-subtle">
|
||||
<.icon name="hero-bug-ant" class="size-4 inline" /> Error tracker
|
||||
</.link>
|
||||
</div>
|
||||
@@ -515,21 +499,17 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
slot :inner_block, required: true
|
||||
|
||||
defp status_pill(assigns) do
|
||||
classes =
|
||||
modifier =
|
||||
case assigns.color do
|
||||
"green" -> "bg-green-50 text-green-700 ring-green-600/20"
|
||||
"amber" -> "bg-amber-50 text-amber-700 ring-amber-600/20"
|
||||
"zinc" -> "bg-base-200/50 text-base-content/60 ring-base-content/10"
|
||||
_ -> "bg-base-200/50 text-base-content/60 ring-base-content/10"
|
||||
"green" -> "admin-status-pill-green"
|
||||
"amber" -> "admin-status-pill-amber"
|
||||
_ -> "admin-status-pill-zinc"
|
||||
end
|
||||
|
||||
assigns = assign(assigns, :classes, classes)
|
||||
assigns = assign(assigns, :modifier, modifier)
|
||||
|
||||
~H"""
|
||||
<span class={[
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||
@classes
|
||||
]}>
|
||||
<span class={["admin-status-pill", @modifier]}>
|
||||
{render_slot(@inner_block)}
|
||||
</span>
|
||||
"""
|
||||
@@ -548,38 +528,38 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
|> assign(:provider_label, String.capitalize(conn.provider_type))
|
||||
|
||||
~H"""
|
||||
<div class="mt-4">
|
||||
<dl class="text-sm">
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Provider</dt>
|
||||
<dd class="text-base-content">{@provider_label}</dd>
|
||||
<div class="admin-section-body">
|
||||
<dl class="admin-dl">
|
||||
<div class="admin-dl-row">
|
||||
<dt class="admin-dl-term">Provider</dt>
|
||||
<dd class="admin-dl-value">{@provider_label}</dd>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Shop</dt>
|
||||
<dd class="text-base-content">{@connection.name}</dd>
|
||||
<div class="admin-dl-row">
|
||||
<dt class="admin-dl-term">Shop</dt>
|
||||
<dd class="admin-dl-value">{@connection.name}</dd>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Products</dt>
|
||||
<dd class="text-base-content">{@product_count}</dd>
|
||||
<div class="admin-dl-row">
|
||||
<dt class="admin-dl-term">Products</dt>
|
||||
<dd class="admin-dl-value">{@product_count}</dd>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Last synced</dt>
|
||||
<dd class="text-base-content">
|
||||
<div class="admin-dl-row">
|
||||
<dt class="admin-dl-term">Last synced</dt>
|
||||
<dd class="admin-dl-value">
|
||||
<%= if @connection.last_synced_at do %>
|
||||
{format_relative_time(@connection.last_synced_at)}
|
||||
<% else %>
|
||||
<span class="text-amber-600">Never</span>
|
||||
<span style="color: #d97706;">Never</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<div class="admin-cluster" style="margin-top: 1rem;">
|
||||
<button
|
||||
phx-click="sync"
|
||||
phx-value-id={@connection.id}
|
||||
disabled={@syncing}
|
||||
class="inline-flex items-center gap-1.5 rounded-md bg-base-200 px-3 py-1.5 text-sm font-medium text-base-content hover:bg-base-300 ring-1 ring-base-300 ring-inset"
|
||||
class="admin-btn admin-btn-outline admin-btn-sm"
|
||||
>
|
||||
<.icon
|
||||
name="hero-arrow-path"
|
||||
@@ -589,7 +569,7 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</button>
|
||||
<.link
|
||||
navigate={~p"/admin/providers/#{@connection.id}/edit"}
|
||||
class="inline-flex items-center gap-1.5 rounded-md bg-base-200 px-3 py-1.5 text-sm font-medium text-base-content hover:bg-base-300 ring-1 ring-base-300 ring-inset"
|
||||
class="admin-btn admin-btn-outline admin-btn-sm"
|
||||
>
|
||||
<.icon name="hero-cog-6-tooth" class="size-4" /> Settings
|
||||
</.link>
|
||||
@@ -597,7 +577,8 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
phx-click="delete_connection"
|
||||
phx-value-id={@connection.id}
|
||||
data-confirm={"Disconnect from #{@provider_label}? Your synced products will remain in your shop."}
|
||||
class="text-sm text-red-600 hover:text-red-800 px-2 py-1.5"
|
||||
class="admin-link-danger"
|
||||
style="padding: 0.375rem 0.5rem;"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
@@ -608,22 +589,17 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
|
||||
defp stripe_setup_form(assigns) do
|
||||
~H"""
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-base-content/60">
|
||||
<div class="admin-section-body">
|
||||
<p class="admin-section-desc" style="margin-top: 0;">
|
||||
To accept payments, connect your Stripe account by entering your secret key.
|
||||
You can find it in your
|
||||
<a
|
||||
href="https://dashboard.stripe.com/apikeys"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-base-content underline"
|
||||
>
|
||||
<a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noopener" class="admin-link">
|
||||
Stripe dashboard
|
||||
</a>
|
||||
under Developers → API keys.
|
||||
</p>
|
||||
|
||||
<.form for={@connect_form} phx-submit="connect_stripe" class="mt-6">
|
||||
<.form for={@connect_form} phx-submit="connect_stripe" style="margin-top: 1.5rem;">
|
||||
<.input
|
||||
field={@connect_form[:api_key]}
|
||||
type="password"
|
||||
@@ -631,11 +607,11 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
autocomplete="off"
|
||||
placeholder="sk_test_... or sk_live_..."
|
||||
/>
|
||||
<p class="text-xs text-base-content/60 mt-1">
|
||||
<p class="admin-text-tertiary" style="margin-top: 0.25rem;">
|
||||
Starts with <code>sk_test_</code> (test mode) or <code>sk_live_</code> (live mode).
|
||||
This key is encrypted at rest in the database.
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<div class="admin-section-body">
|
||||
<.button phx-disable-with="Connecting...">
|
||||
Connect Stripe
|
||||
</.button>
|
||||
@@ -647,40 +623,40 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
|
||||
defp stripe_connected_view(assigns) do
|
||||
~H"""
|
||||
<div class="mt-4 space-y-4">
|
||||
<dl class="text-sm">
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">API key</dt>
|
||||
<dd><code class="text-base-content">{@stripe_api_key_hint}</code></dd>
|
||||
<div class="admin-stack" style="margin-top: 1rem;">
|
||||
<dl class="admin-dl">
|
||||
<div class="admin-dl-row">
|
||||
<dt class="admin-dl-term">API key</dt>
|
||||
<dd class="admin-dl-value"><code>{@stripe_api_key_hint}</code></dd>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Webhook URL</dt>
|
||||
<dd><code class="text-base-content text-xs break-all">{@stripe_webhook_url}</code></dd>
|
||||
<div class="admin-dl-row">
|
||||
<dt class="admin-dl-term">Webhook URL</dt>
|
||||
<dd class="admin-dl-value"><code style="font-size: 0.75rem; word-break: break-all;">{@stripe_webhook_url}</code></dd>
|
||||
</div>
|
||||
<div class="flex gap-2 py-1">
|
||||
<dt class="text-base-content/60 w-28 shrink-0">Webhook secret</dt>
|
||||
<dd>
|
||||
<div class="admin-dl-row">
|
||||
<dt class="admin-dl-term">Webhook secret</dt>
|
||||
<dd class="admin-dl-value">
|
||||
<%= if @stripe_has_signing_secret do %>
|
||||
<code class="text-base-content">{@stripe_signing_secret_hint}</code>
|
||||
<code>{@stripe_signing_secret_hint}</code>
|
||||
<% else %>
|
||||
<span class="text-amber-600">Not set</span>
|
||||
<span style="color: #d97706;">Not set</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<%= if @stripe_status == :connected_localhost do %>
|
||||
<div class="rounded-md bg-amber-50 p-4 ring-1 ring-amber-600/10 ring-inset">
|
||||
<p class="text-sm text-amber-800">
|
||||
<div class="admin-info-box admin-info-box-amber">
|
||||
<p>
|
||||
Stripe can't reach localhost for webhooks. For local testing, run the Stripe CLI:
|
||||
</p>
|
||||
<pre class="mt-2 rounded bg-amber-100 p-2 text-xs text-amber-900 overflow-x-auto">stripe listen --forward-to localhost:4000/webhooks/stripe</pre>
|
||||
<p class="mt-2 text-xs text-amber-700">
|
||||
<pre>stripe listen --forward-to localhost:4000/webhooks/stripe</pre>
|
||||
<p style="margin-top: 0.5rem; font-size: 0.75rem; color: #b45309;">
|
||||
The CLI will output a signing secret starting with <code>whsec_</code>. Enter it below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<.form for={@secret_form} phx-submit="save_signing_secret" class="mt-2">
|
||||
<.form for={@secret_form} phx-submit="save_signing_secret" style="margin-top: 0.5rem;">
|
||||
<.input
|
||||
field={@secret_form[:signing_secret]}
|
||||
type="password"
|
||||
@@ -688,16 +664,13 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
autocomplete="off"
|
||||
placeholder="whsec_..."
|
||||
/>
|
||||
<div class="mt-3">
|
||||
<div style="margin-top: 0.75rem;">
|
||||
<.button phx-disable-with="Saving...">Save signing secret</.button>
|
||||
</div>
|
||||
</.form>
|
||||
<% else %>
|
||||
<div class="border-t border-base-200 pt-3">
|
||||
<button
|
||||
phx-click="toggle_stripe_advanced"
|
||||
class="flex items-center gap-1 text-sm text-base-content/60 hover:text-base-content"
|
||||
>
|
||||
<div style="border-top: 1px solid var(--t-surface-sunken); padding-top: 0.75rem;">
|
||||
<button phx-click="toggle_stripe_advanced" class="admin-link-subtle admin-row" style="--admin-row-gap: 0.25rem;">
|
||||
<.icon
|
||||
name={if @advanced_open, do: "hero-chevron-down-mini", else: "hero-chevron-right-mini"}
|
||||
class="size-4"
|
||||
@@ -705,8 +678,8 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</button>
|
||||
|
||||
<%= if @advanced_open do %>
|
||||
<div class="mt-3">
|
||||
<p class="text-xs text-base-content/60 mb-3">
|
||||
<div style="margin-top: 0.75rem;">
|
||||
<p class="admin-text-tertiary" style="margin-bottom: 0.75rem;">
|
||||
Override the webhook signing secret if you need to use a custom endpoint or the Stripe CLI.
|
||||
</p>
|
||||
<.form for={@secret_form} phx-submit="save_signing_secret">
|
||||
@@ -717,7 +690,7 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
autocomplete="off"
|
||||
placeholder="whsec_..."
|
||||
/>
|
||||
<div class="mt-3">
|
||||
<div style="margin-top: 0.75rem;">
|
||||
<.button phx-disable-with="Saving...">Save signing secret</.button>
|
||||
</div>
|
||||
</.form>
|
||||
@@ -726,11 +699,11 @@ defmodule BerrypodWeb.Admin.Settings do
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="border-t border-base-200 pt-4">
|
||||
<div style="border-top: 1px solid var(--t-surface-sunken); padding-top: 1rem;">
|
||||
<button
|
||||
phx-click="disconnect_stripe"
|
||||
data-confirm="This will remove your Stripe API key and delete the webhook endpoint. Are you sure?"
|
||||
class="text-sm text-red-600 hover:text-red-800"
|
||||
class="admin-link-danger"
|
||||
>
|
||||
Disconnect Stripe
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user