complete admin CSS refactor: delete utilities.css, add layout primitives
- Delete utilities.css (701 lines / 24 KB of Tailwind utility clones) - Add layout.css with admin-stack, admin-row, admin-cluster, admin-grid primitives and gap variants (sm, md, lg, xl) - Add transitions.css import and layout.css import to admin.css entry point - Replace all Tailwind utility classes across 26 admin templates with semantic admin-*/theme-*/page-specific CSS classes - Replace all non-dynamic inline styles with semantic classes - Add ~100 new semantic classes to components.css (analytics, dashboard, order detail, settings, theme editor, generic utilities) - Fix stray text-error → admin-text-error in media.ex - Add missing .truncate definition to admin CSS - Only remaining inline styles are dynamic data values (progress bars, chart dimensions) and one JS.toggle target Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -144,10 +144,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
<.header>Analytics</.header>
|
||||
|
||||
<%!-- Period selector --%>
|
||||
<div
|
||||
class="analytics-periods"
|
||||
style="display: flex; gap: 0.25rem; margin-top: 1rem; align-items: center;"
|
||||
>
|
||||
<div class="analytics-periods">
|
||||
<button
|
||||
:for={period <- ["today", "7d", "30d", "12m"]}
|
||||
phx-click={
|
||||
@@ -162,8 +159,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
id="analytics-export-link"
|
||||
data-period={@period}
|
||||
href={export_url(@period, @filters)}
|
||||
class="admin-btn admin-btn-sm"
|
||||
style="margin-left: auto;"
|
||||
class="admin-btn admin-btn-sm analytics-export"
|
||||
phx-hook="AnalyticsExport"
|
||||
>
|
||||
Export CSV
|
||||
@@ -174,7 +170,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
<div
|
||||
:if={@filters != %{}}
|
||||
id="analytics-filters"
|
||||
style="display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin-top: 1rem;"
|
||||
class="analytics-filters"
|
||||
>
|
||||
<.filter_chip
|
||||
:for={{dim, val} <- @filters}
|
||||
@@ -184,15 +180,14 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
<button
|
||||
:if={map_size(@filters) > 1}
|
||||
phx-click="clear_filters"
|
||||
class="admin-btn admin-btn-sm"
|
||||
style="font-size: 0.75rem;"
|
||||
class="admin-btn admin-btn-sm admin-text-secondary"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%!-- Stat cards --%>
|
||||
<div class="admin-stats-grid" style="margin-top: 1.5rem;">
|
||||
<div class="admin-stats-grid admin-card-spaced">
|
||||
<.stat_card
|
||||
label="Unique visitors"
|
||||
value={format_number(@visitors)}
|
||||
@@ -221,15 +216,15 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
</div>
|
||||
|
||||
<%!-- Visitor trend chart --%>
|
||||
<div class="admin-card" style="margin-top: 1.5rem; padding: 1rem;">
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.75rem;">
|
||||
<div class="admin-card admin-card-spaced analytics-chart-card">
|
||||
<h3 class="analytics-tab-heading">
|
||||
Visitors over time
|
||||
</h3>
|
||||
<.bar_chart data={@trend_data} mode={@trend_mode} />
|
||||
</div>
|
||||
|
||||
<%!-- Detail tabs --%>
|
||||
<div style="display: flex; gap: 0.25rem; margin-top: 1.5rem; flex-wrap: wrap;">
|
||||
<div class="analytics-tab-bar">
|
||||
<button
|
||||
:for={
|
||||
tab <- [
|
||||
@@ -249,7 +244,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
</div>
|
||||
|
||||
<%!-- Tab content --%>
|
||||
<div class="admin-card" style="margin-top: 0.75rem; padding: 1rem;">
|
||||
<div class="admin-card analytics-tab-panel">
|
||||
<.tab_content tab={@tab} {assigns} />
|
||||
</div>
|
||||
"""
|
||||
@@ -266,18 +261,16 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
defp stat_card(assigns) do
|
||||
~H"""
|
||||
<div class="admin-card">
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem; padding: 1rem;">
|
||||
<div style="background: var(--color-base-200, #e5e5e5); border-radius: 0.5rem; padding: 0.5rem;">
|
||||
<div class="admin-stat-card-body">
|
||||
<div class="admin-stat-icon">
|
||||
<.icon name={@icon} class="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div style="display: flex; align-items: baseline; gap: 0.5rem;">
|
||||
<p style="font-size: 1.5rem; font-weight: 700;">{@value}</p>
|
||||
<div class="admin-stat-value-row">
|
||||
<p class="admin-stat-value">{@value}</p>
|
||||
<.delta_badge :if={@delta != nil} delta={@delta} invert={@invert} />
|
||||
</div>
|
||||
<p style="font-size: 0.875rem; color: color-mix(in oklch, var(--color-base-content) 60%, transparent);">
|
||||
{@label}
|
||||
</p>
|
||||
<p class="admin-stat-label">{@label}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,26 +282,24 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
|
||||
defp delta_badge(%{delta: :new} = assigns) do
|
||||
~H"""
|
||||
<span style="font-size: 0.75rem; font-weight: 500; color: color-mix(in oklch, var(--color-base-content) 50%, transparent); white-space: nowrap;">
|
||||
new
|
||||
</span>
|
||||
<span class="analytics-delta admin-text-secondary">new</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp delta_badge(assigns) do
|
||||
{color, arrow} =
|
||||
{color_class, arrow} =
|
||||
cond do
|
||||
assigns.delta > 0 && !assigns.invert -> {"var(--t-status-success, #22c55e)", "↑"}
|
||||
assigns.delta > 0 && assigns.invert -> {"var(--t-status-error, #ef4444)", "↑"}
|
||||
assigns.delta < 0 && !assigns.invert -> {"var(--t-status-error, #ef4444)", "↓"}
|
||||
assigns.delta < 0 && assigns.invert -> {"var(--t-status-success, #22c55e)", "↓"}
|
||||
true -> {"color-mix(in oklch, var(--color-base-content) 40%, transparent)", "–"}
|
||||
assigns.delta > 0 && !assigns.invert -> {"admin-icon-positive", "↑"}
|
||||
assigns.delta > 0 && assigns.invert -> {"admin-text-error", "↑"}
|
||||
assigns.delta < 0 && !assigns.invert -> {"admin-text-error", "↓"}
|
||||
assigns.delta < 0 && assigns.invert -> {"admin-icon-positive", "↓"}
|
||||
true -> {"admin-text-secondary", "–"}
|
||||
end
|
||||
|
||||
assigns = assign(assigns, color: color, arrow: arrow)
|
||||
assigns = assign(assigns, color_class: color_class, arrow: arrow)
|
||||
|
||||
~H"""
|
||||
<span style={"font-size: 0.75rem; font-weight: 500; color: #{@color}; white-space: nowrap;"}>
|
||||
<span class={["analytics-delta", @color_class]}>
|
||||
{@arrow} {if abs(@delta) > 999, do: ">999%", else: "#{abs(@delta)}%"}
|
||||
</span>
|
||||
"""
|
||||
@@ -326,13 +317,13 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
<span
|
||||
data-filter-dimension={@dimension}
|
||||
data-filter-value={@value}
|
||||
style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.25rem 0.5rem; font-size: 0.75rem; background: var(--color-base-200, #e5e5e5); border-radius: 0.25rem;"
|
||||
class="analytics-filter-chip"
|
||||
>
|
||||
{@label}
|
||||
<button
|
||||
phx-click="remove_filter"
|
||||
phx-value-dimension={@dimension}
|
||||
style="cursor: pointer; opacity: 0.6; line-height: 1;"
|
||||
class="analytics-filter-remove"
|
||||
aria-label={"Remove #{@label} filter"}
|
||||
>
|
||||
<.icon name="hero-x-mark" class="size-3" />
|
||||
@@ -430,59 +421,42 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
:if={@data == []}
|
||||
style="text-align: center; padding: 2rem; color: color-mix(in oklch, var(--color-base-content) 40%, transparent);"
|
||||
>
|
||||
<div :if={@data == []} class="analytics-empty">
|
||||
No data for this period
|
||||
</div>
|
||||
<div
|
||||
:if={@data != []}
|
||||
id="analytics-chart"
|
||||
phx-hook="ChartTooltip"
|
||||
style="display: grid; grid-template-columns: auto 1fr; gap: 0 0.5rem; position: relative;"
|
||||
class="analytics-chart-grid"
|
||||
>
|
||||
<%!-- Tooltip --%>
|
||||
<div
|
||||
data-tooltip
|
||||
style="display: none; position: absolute; top: -1.75rem; z-index: 10; padding: 0.25rem 0.5rem; font-size: 0.75rem; font-weight: 500; white-space: nowrap; background: var(--color-base-content, #1e1e1e); color: var(--color-base-100, #fff); border-radius: 0.25rem; pointer-events: none; font-variant-numeric: tabular-nums;"
|
||||
>
|
||||
</div>
|
||||
<div data-tooltip class="analytics-tooltip"></div>
|
||||
<%!-- Row 1: Y-axis labels + chart --%>
|
||||
<div
|
||||
class="analytics-y-labels"
|
||||
style="display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; height: 8rem;"
|
||||
>
|
||||
<div class="analytics-y-labels">
|
||||
<span>{format_number(@scale_max)}</span>
|
||||
<span>{format_number(@scale_mid)}</span>
|
||||
<span>0</span>
|
||||
</div>
|
||||
<div style="position: relative; height: 8rem;">
|
||||
<div class="analytics-chart-area">
|
||||
<%!-- Gridlines --%>
|
||||
<div style="position: absolute; top: 50%; left: 0; right: 0; border-top: 1px dashed color-mix(in oklch, var(--color-base-content) 12%, transparent);">
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 0; left: 0; right: 0; border-top: 1px solid color-mix(in oklch, var(--color-base-content) 15%, transparent);">
|
||||
</div>
|
||||
<div class="analytics-gridline-mid"></div>
|
||||
<div class="analytics-gridline-bottom"></div>
|
||||
<%!-- Bars container --%>
|
||||
<div
|
||||
data-bars
|
||||
style={"display: flex; align-items: flex-end; height: 100%; gap: #{@bar_gap}px;"}
|
||||
>
|
||||
<div data-bars class="analytics-bars" style={"gap: #{@bar_gap}px;"}>
|
||||
<div
|
||||
:for={bar <- @bars}
|
||||
data-label={bar.label}
|
||||
data-visitors={bar.visitors}
|
||||
style={"flex: 1; height: #{max(bar.height_pct, 0.5)}%; background: var(--color-primary, #4f46e5); opacity: 0.8; border-radius: 1px 1px 0 0; min-width: 0;"}
|
||||
class="analytics-bar"
|
||||
style={"height: #{max(bar.height_pct, 0.5)}%;"}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%!-- Row 2: empty cell + X-axis labels --%>
|
||||
<div></div>
|
||||
<div
|
||||
class="analytics-x-labels"
|
||||
style="display: flex; justify-content: space-between; padding-top: 0.25rem;"
|
||||
>
|
||||
<div class="analytics-x-labels">
|
||||
<span :for={bar <- @x_labels}>{bar.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -519,7 +493,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
|
||||
defp tab_content(%{tab: "pages"} = assigns) do
|
||||
~H"""
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.75rem;">Top pages</h3>
|
||||
<h3 class="analytics-tab-heading">Top pages</h3>
|
||||
<.detail_table
|
||||
rows={@top_pages}
|
||||
empty_message="No page data yet"
|
||||
@@ -529,7 +503,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
%{label: "Pageviews", key: :pageviews, align: :right}
|
||||
]}
|
||||
/>
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin: 1.5rem 0 0.75rem;">Entry pages</h3>
|
||||
<h3 class="analytics-tab-heading-spaced">Entry pages</h3>
|
||||
<.detail_table
|
||||
rows={@entry_pages}
|
||||
empty_message="No entry page data yet"
|
||||
@@ -538,7 +512,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
%{label: "Sessions", key: :sessions, align: :right}
|
||||
]}
|
||||
/>
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin: 1.5rem 0 0.75rem;">Exit pages</h3>
|
||||
<h3 class="analytics-tab-heading-spaced">Exit pages</h3>
|
||||
<.detail_table
|
||||
rows={@exit_pages}
|
||||
empty_message="No exit page data yet"
|
||||
@@ -552,7 +526,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
|
||||
defp tab_content(%{tab: "sources"} = assigns) do
|
||||
~H"""
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.75rem;">Top sources</h3>
|
||||
<h3 class="analytics-tab-heading">Top sources</h3>
|
||||
<.detail_table
|
||||
rows={@top_sources}
|
||||
empty_message="No referrer data yet"
|
||||
@@ -561,7 +535,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
%{label: "Visitors", key: :visitors, align: :right}
|
||||
]}
|
||||
/>
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin: 1.5rem 0 0.75rem;">Top referrers</h3>
|
||||
<h3 class="analytics-tab-heading-spaced">Top referrers</h3>
|
||||
<.detail_table
|
||||
rows={@top_referrers}
|
||||
empty_message="No referrer data yet"
|
||||
@@ -582,7 +556,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
assigns = assign(assigns, :country_rows, rows)
|
||||
|
||||
~H"""
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.75rem;">Countries</h3>
|
||||
<h3 class="analytics-tab-heading">Countries</h3>
|
||||
<.detail_table
|
||||
rows={@country_rows}
|
||||
empty_message="No country data yet"
|
||||
@@ -596,7 +570,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
|
||||
defp tab_content(%{tab: "devices"} = assigns) do
|
||||
~H"""
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.75rem;">Browsers</h3>
|
||||
<h3 class="analytics-tab-heading">Browsers</h3>
|
||||
<.detail_table
|
||||
rows={@browsers}
|
||||
empty_message="No browser data yet"
|
||||
@@ -605,9 +579,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
%{label: "Visitors", key: :visitors, align: :right}
|
||||
]}
|
||||
/>
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin: 1.5rem 0 0.75rem;">
|
||||
Operating systems
|
||||
</h3>
|
||||
<h3 class="analytics-tab-heading-spaced">Operating systems</h3>
|
||||
<.detail_table
|
||||
rows={@oses}
|
||||
empty_message="No OS data yet"
|
||||
@@ -616,7 +588,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
%{label: "Visitors", key: :visitors, align: :right}
|
||||
]}
|
||||
/>
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin: 1.5rem 0 0.75rem;">Screen sizes</h3>
|
||||
<h3 class="analytics-tab-heading-spaced">Screen sizes</h3>
|
||||
<.detail_table
|
||||
rows={@screen_sizes}
|
||||
empty_message="No screen data yet"
|
||||
@@ -630,9 +602,7 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
|
||||
defp tab_content(%{tab: "funnel"} = assigns) do
|
||||
~H"""
|
||||
<h3 style="font-size: 0.875rem; font-weight: 600; margin-bottom: 0.75rem;">
|
||||
Conversion funnel
|
||||
</h3>
|
||||
<h3 class="analytics-tab-heading">Conversion funnel</h3>
|
||||
<.funnel_chart funnel={@funnel} revenue={@revenue} />
|
||||
"""
|
||||
end
|
||||
@@ -645,18 +615,15 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
|
||||
defp detail_table(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
:if={@rows == []}
|
||||
style="text-align: center; padding: 1.5rem; color: color-mix(in oklch, var(--color-base-content) 40%, transparent);"
|
||||
>
|
||||
<div :if={@rows == []} class="analytics-empty">
|
||||
{@empty_message}
|
||||
</div>
|
||||
<table :if={@rows != []} class="admin-table" style="width: 100%;">
|
||||
<table :if={@rows != []} class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
:for={col <- @columns}
|
||||
style={col[:align] == :right && "text-align: right;"}
|
||||
class={col[:align] == :right && "admin-cell-end"}
|
||||
>
|
||||
{col.label}
|
||||
</th>
|
||||
@@ -666,12 +633,11 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
<tr :for={row <- @rows}>
|
||||
<td
|
||||
:for={col <- @columns}
|
||||
style={col[:align] == :right && "text-align: right; font-variant-numeric: tabular-nums;"}
|
||||
class={col[:align] == :right && "admin-cell-numeric"}
|
||||
>
|
||||
<%= if col[:filter] do %>
|
||||
<span
|
||||
class="admin-link"
|
||||
style="cursor: pointer;"
|
||||
phx-click="add_filter"
|
||||
phx-value-dimension={elem(col.filter, 0)}
|
||||
phx-value-value={Map.get(row, elem(col.filter, 1))}
|
||||
@@ -721,35 +687,25 @@ defmodule BerrypodWeb.Admin.Analytics do
|
||||
assigns = assign(assigns, steps: steps_with_rates, conversion_rate: conversion_rate)
|
||||
|
||||
~H"""
|
||||
<div
|
||||
:if={@funnel.product_views == 0}
|
||||
style="text-align: center; padding: 1.5rem; color: color-mix(in oklch, var(--color-base-content) 40%, transparent);"
|
||||
>
|
||||
<div :if={@funnel.product_views == 0} class="analytics-empty">
|
||||
No funnel data yet
|
||||
</div>
|
||||
<div :if={@funnel.product_views > 0} style="display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<div :for={step <- @steps} style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<div style="width: 7rem; font-size: 0.8125rem; text-align: right; flex-shrink: 0;">
|
||||
{step.label}
|
||||
</div>
|
||||
<div style={"flex: 0 0 #{step.width_pct}%; height: 2rem; background: var(--color-primary, #4f46e5); border-radius: 0.25rem; opacity: #{1 - step.index * 0.15}; display: flex; align-items: center; padding-left: 0.5rem;"}>
|
||||
<span style="font-size: 0.75rem; font-weight: 600; color: white;">
|
||||
{format_number(step.count)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
:if={step.index > 0}
|
||||
style="font-size: 0.8125rem; font-weight: 600;"
|
||||
<div :if={@funnel.product_views > 0} class="analytics-funnel">
|
||||
<div :for={step <- @steps} class="analytics-funnel-step">
|
||||
<div class="analytics-funnel-label">{step.label}</div>
|
||||
<div
|
||||
class="analytics-funnel-bar"
|
||||
style={"flex: 0 0 #{step.width_pct}%; opacity: #{1 - step.index * 0.15};"}
|
||||
>
|
||||
<span class="analytics-funnel-value">{format_number(step.count)}</span>
|
||||
</div>
|
||||
<span :if={step.index > 0} class="analytics-funnel-rate">
|
||||
{step.overall_rate}%
|
||||
</span>
|
||||
</div>
|
||||
<div style="margin-top: 0.75rem; font-size: 0.875rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<span style="font-weight: 600;">{@conversion_rate}% overall conversion</span>
|
||||
<span
|
||||
:if={@revenue > 0}
|
||||
style="color: color-mix(in oklch, var(--color-base-content) 60%, transparent);"
|
||||
>
|
||||
<div class="analytics-funnel-summary">
|
||||
<span class="admin-text-bold">{@conversion_rate}% overall conversion</span>
|
||||
<span :if={@revenue > 0} class="admin-text-secondary">
|
||||
· Revenue: {Cart.format_price(@revenue)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user