berrypod/lib/berrypod_web/live/admin/theme/index.html.heex
jamey 93ff66debc
Some checks failed
deploy / deploy (push) Has been cancelled
add legal page editor integration and media library polish
Legal pages (privacy, delivery, terms) now auto-populate content from
shop settings on mount, show auto-generated vs customised badges, and
have a regenerate button. Theme editor gains alt text fields for logo,
header, and icon images. Image picker in page builder now has an upload
button and alt text warning badges. Clearing unused image references
shows an orphan info flash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:55:02 +00:00

1367 lines
57 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div class="min-h-screen bg-base-200">
<div class="flex flex-col lg:flex-row lg:h-screen">
<!-- Controls Sidebar -->
<div
id="theme-sidebar"
class={[
"bg-base-100 border-r border-base-300 lg:h-screen flex-shrink-0 transition-all duration-300",
if(@sidebar_collapsed,
do: "w-12 overflow-hidden",
else: "w-full lg:w-[380px] overflow-y-auto p-6"
)
]}
>
<!-- Collapsed state: just show expand button -->
<%= if @sidebar_collapsed do %>
<div class="h-full flex items-start justify-center pt-4">
<button
type="button"
phx-click="toggle_sidebar"
class="p-2 rounded-lg hover:bg-base-200 transition-colors"
aria-label="Expand sidebar"
aria-expanded="false"
aria-controls="theme-sidebar"
>
<svg
class="w-5 h-5 text-base-content/70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
<% else %>
<.link
href={~p"/admin"}
class="inline-flex items-center gap-1 text-sm text-base-content/60 hover:text-base-content mb-4"
>
<.icon name="hero-arrow-left-mini" class="size-4" /> Admin
</.link>
<!-- Header -->
<div class="mb-6 flex items-start justify-between gap-3">
<div class="flex-1">
<h1 class="text-xl font-semibold tracking-tight mb-2 text-base-content">
Theme Studio
</h1>
<p class="text-sm text-base-content/60 leading-relaxed">
One theme, infinite possibilities. Every combination is designed to work beautifully.
</p>
</div>
<button
type="button"
phx-click="toggle_sidebar"
class="p-2 rounded-lg hover:bg-base-200 transition-colors flex-shrink-0 -mr-2 -mt-1"
aria-label="Collapse sidebar"
aria-expanded="true"
aria-controls="theme-sidebar"
>
<svg
class="w-5 h-5 text-base-content/70"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
</div>
<!-- Site Name -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Shop name
</label>
<form phx-change="update_setting" phx-value-field="site_name">
<input
type="text"
name="site_name"
value={@theme_settings.site_name}
placeholder="Your shop name"
class="w-full px-4 py-3 border border-base-300 rounded-lg text-base bg-base-100 text-base-content focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-all"
/>
</form>
</div>
<!-- Branding Section (styled background box) -->
<div class="bg-base-200 rounded-xl p-4 mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-4">
Logo & header
</label>
<!-- Logo Mode Radio Cards -->
<div class="flex flex-col gap-2 mb-4">
<%= for {value, title, desc} <- [
{"text-only", "Shop name only", "Your name in the heading font"},
{"logo-text", "Logo + shop name", "Your logo image with name beside it"},
{"logo-only", "Logo only", "Just your logo (with text built in)"}
] do %>
<label class={[
"flex items-center gap-3 p-3 bg-base-100 border-2 rounded-lg cursor-pointer transition-all",
if(@theme_settings.logo_mode == value,
do: "border-base-content",
else: "border-transparent hover:border-base-300"
)
]}>
<input
type="radio"
name="logo_mode"
value={value}
checked={@theme_settings.logo_mode == value}
phx-click="update_setting"
phx-value-field="logo_mode"
phx-value-setting_value={value}
class="hidden"
/>
<span class={[
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 transition-all",
if(@theme_settings.logo_mode == value,
do: "border-base-content",
else: "border-base-300"
)
]}>
<span class={[
"w-2 h-2 rounded-full bg-base-content transition-all",
if(@theme_settings.logo_mode == value,
do: "scale-100 opacity-100",
else: "scale-0 opacity-0"
)
]}>
</span>
</span>
<div class="flex-1">
<div class="text-sm font-medium text-base-content">{title}</div>
<div class="text-xs text-base-content/60">{desc}</div>
</div>
</label>
<% end %>
</div>
<!-- Logo Upload (for logo-text and logo-only modes) -->
<%= if @theme_settings.logo_mode in ["logo-text", "logo-only"] do %>
<div class="mt-4 pt-4 border-t border-base-300">
<span class="block text-xs font-medium text-base-content/70 mb-2">
Upload logo (SVG or PNG)
</span>
<div class="flex items-center gap-3">
<form phx-change="noop" phx-submit="noop" class="flex-1">
<label class="flex-1 bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all">
<span>Choose file...</span>
<.live_file_input upload={@uploads.logo_upload} class="hidden" />
</label>
</form>
<%= if @logo_image do %>
<div class="relative w-16 h-10 bg-base-100 border border-base-300 rounded-lg flex items-center justify-center overflow-hidden">
<img
src={"/image_cache/#{@logo_image.id}.webp"}
alt="Current logo"
class="max-w-full max-h-full object-contain"
/>
<button
type="button"
phx-click="remove_logo"
class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none"
title="Remove logo"
>
×
</button>
</div>
<form
phx-change="update_image_alt"
phx-value-image-id={@logo_image.id}
class="mt-2"
>
<label class="flex items-center gap-2">
<span class="text-xs text-base-content/60 shrink-0">Alt text</span>
<input
type="text"
name="alt"
value={@logo_image.alt || ""}
placeholder="Describe this image"
class="admin-input admin-input-sm flex-1"
phx-debounce="blur"
/>
</label>
<p
:if={!@logo_image.alt || @logo_image.alt == ""}
class="text-xs text-warning mt-1"
>
Missing alt text — add a description for accessibility
</p>
</form>
<% end %>
</div>
<%= for entry <- @uploads.logo_upload.entries do %>
<div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
<div
class="h-full bg-primary transition-all"
style={"width: #{entry.progress}%"}
>
</div>
</div>
<span class="text-xs text-base-content/60">{entry.progress}%</span>
<button
type="button"
phx-click="cancel_upload"
phx-value-ref={entry.ref}
phx-value-upload="logo_upload"
class="text-base-content/40 hover:text-base-content/70"
>
×
</button>
</div>
<%= for err <- upload_errors(@uploads.logo_upload, entry) do %>
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.logo_upload) do %>
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %>
<!-- Logo Size Slider -->
<%= if @logo_image do %>
<form phx-change="update_setting" phx-value-field="logo_size" class="mt-3">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Logo size</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.logo_size}px
</span>
</div>
<input
type="range"
min="24"
max="120"
value={@theme_settings.logo_size}
name="logo_size"
class="admin-range w-full"
/>
</form>
<!-- SVG Recolor Toggle (only for SVG logos) -->
<%= if @logo_image.is_svg do %>
<div class="mt-3">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.logo_recolor}
phx-click="update_setting"
phx-value-field="logo_recolor"
phx-value-setting_value={
if @theme_settings.logo_recolor, do: "false", else: "true"
}
class="admin-toggle admin-toggle-sm"
/>
<span class="text-sm text-base-content/70">Recolour logo</span>
</label>
<%= if @theme_settings.logo_recolor do %>
<form
id="logo-color-form"
phx-change="update_color"
phx-value-field="logo_color"
phx-hook="ColorSync"
class="flex items-center gap-3 mt-2"
>
<input
type="color"
name="value"
value={@theme_settings.logo_color}
class="w-9 h-9 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70">
{@theme_settings.logo_color}
</span>
</form>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
<!-- Site Icon / Favicon -->
<div class="bg-base-200 rounded-xl p-4 mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Site icon
</label>
<p class="text-xs text-base-content/50 mb-4">
Your icon appears in browser tabs and on home screens.
</p>
<!-- Use logo as icon toggle -->
<label class="flex items-center gap-2 cursor-pointer mb-4">
<input
type="checkbox"
checked={@theme_settings.use_logo_as_icon}
phx-click="toggle_setting"
phx-value-field="use_logo_as_icon"
class="admin-toggle admin-toggle-sm"
/>
<span class="text-sm text-base-content/70">Use logo as favicon</span>
</label>
<!-- Icon upload (only when not using logo) -->
<%= if !@theme_settings.use_logo_as_icon do %>
<div class="pt-3 border-t border-base-300">
<span class="block text-xs font-medium text-base-content/70 mb-2">
Upload icon (PNG or SVG, 512×512+)
</span>
<div class="flex items-center gap-3">
<form phx-change="noop" phx-submit="noop" class="flex-1">
<label class="flex-1 bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all">
<span>Choose file...</span>
<.live_file_input upload={@uploads.icon_upload} class="hidden" />
</label>
</form>
<%= if @icon_image do %>
<div class="relative w-10 h-10 bg-base-100 border border-base-300 rounded-lg flex items-center justify-center overflow-hidden">
<%= if @icon_image.is_svg do %>
<img
src={"/images/#{@icon_image.id}/recolored/000000"}
alt="Current icon"
class="max-w-full max-h-full object-contain"
/>
<% else %>
<img
src={"/image_cache/#{@icon_image.id}.webp"}
alt="Current icon"
class="max-w-full max-h-full object-contain"
/>
<% end %>
<button
type="button"
phx-click="remove_icon"
class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none"
title="Remove icon"
>
×
</button>
</div>
<% end %>
</div>
<%= for entry <- @uploads.icon_upload.entries do %>
<div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
<div
class="h-full bg-primary transition-all"
style={"width: #{entry.progress}%"}
>
</div>
</div>
<span class="text-xs text-base-content/60">{entry.progress}%</span>
<button
type="button"
phx-click="cancel_upload"
phx-value-ref={entry.ref}
phx-value-upload="icon_upload"
class="text-base-content/40 hover:text-base-content/70"
>
×
</button>
</div>
<%= for err <- upload_errors(@uploads.icon_upload, entry) do %>
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.icon_upload) do %>
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %>
</div>
<% end %>
<!-- Short name -->
<div class="mt-4 pt-3 border-t border-base-300">
<form phx-change="update_setting" phx-value-field="favicon_short_name">
<div class="flex justify-between items-center mb-1">
<span class="text-xs font-medium text-base-content/70">Short name</span>
<span class="text-xs text-base-content/50">Home screen label</span>
</div>
<input
type="text"
name="favicon_short_name"
value={@theme_settings.favicon_short_name}
placeholder={String.slice(@theme_settings.site_name, 0, 12)}
maxlength="12"
class="w-full px-3 py-2 border border-base-300 rounded-lg text-sm bg-base-100 text-base-content focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-all"
/>
</form>
</div>
<!-- Icon background colour -->
<div class="mt-3">
<form
id="icon-bg-color-form"
phx-change="update_color"
phx-value-field="icon_background_color"
phx-hook="ColorSync"
class="flex items-center gap-3"
>
<input
type="color"
name="value"
value={@theme_settings.icon_background_color}
class="w-9 h-9 rounded-lg cursor-pointer border-0 p-0"
/>
<div>
<span class="text-xs font-medium text-base-content/70 block">
Icon background
</span>
<span class="font-mono text-xs text-base-content/50">
{@theme_settings.icon_background_color}
</span>
</div>
</form>
</div>
</div>
<!-- Header Background Toggle -->
<div class="mb-6">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.header_background_enabled}
phx-click="update_setting"
phx-value-field="header_background_enabled"
phx-value-setting_value={
if @theme_settings.header_background_enabled, do: "false", else: "true"
}
class="admin-toggle admin-toggle-sm"
/>
<span class="text-sm text-base-content/80">Header background image</span>
</label>
</div>
<!-- Header Image Upload (only when enabled) -->
<%= if @theme_settings.header_background_enabled do %>
<div class="bg-base-200 rounded-xl p-4 mb-6">
<span class="block text-xs font-medium text-base-content/70 mb-2">
Upload header image
</span>
<form phx-change="noop" phx-submit="noop">
<label class="block bg-base-100 border border-dashed border-base-300 rounded-lg p-3 text-sm text-base-content/60 text-center cursor-pointer hover:border-base-content/40 hover:text-base-content/80 transition-all">
<span>Choose file...</span>
<.live_file_input upload={@uploads.header_upload} class="hidden" />
</label>
</form>
<%= if @header_image do %>
<div class="relative w-full h-[60px] bg-base-100 border border-base-300 rounded-lg mt-2 overflow-hidden">
<img
src={"/image_cache/#{@header_image.id}.webp"}
alt="Current header background"
class="w-full h-full object-cover"
/>
<button
type="button"
phx-click="remove_header"
class="absolute -top-1.5 -right-1.5 w-[18px] h-[18px] bg-base-content text-base-100 rounded-full text-xs flex items-center justify-center leading-none"
title="Remove header background"
>
×
</button>
</div>
<form
phx-change="update_image_alt"
phx-value-image-id={@header_image.id}
class="mt-2"
>
<label class="flex items-center gap-2">
<span class="text-xs text-base-content/60 shrink-0">Alt text</span>
<input
type="text"
name="alt"
value={@header_image.alt || ""}
placeholder="Describe this image"
class="admin-input admin-input-sm flex-1"
phx-debounce="blur"
/>
</label>
<p
:if={!@header_image.alt || @header_image.alt == ""}
class="text-xs text-warning mt-1"
>
Missing alt text — add a description for accessibility
</p>
</form>
<!-- Header Image Controls -->
<div class="mt-3 flex flex-col gap-3">
<form phx-change="update_setting" phx-value-field="header_zoom">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">Zoom</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_zoom}%
</span>
</div>
<input
type="range"
min="100"
max="200"
value={@theme_settings.header_zoom}
name="header_zoom"
class="admin-range w-full"
/>
</form>
<form phx-change="update_setting" phx-value-field="header_position_x">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">
Horizontal position
</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_position_x}%
</span>
</div>
<input
type="range"
min="0"
max="100"
value={@theme_settings.header_position_x}
name="header_position_x"
class="admin-range w-full"
/>
</form>
<form phx-change="update_setting" phx-value-field="header_position_y">
<div class="flex justify-between items-center mb-2">
<span class="text-xs font-medium text-base-content/70">
Vertical position
</span>
<span class="text-xs font-mono text-base-content/60">
{@theme_settings.header_position_y}%
</span>
</div>
<input
type="range"
min="0"
max="100"
value={@theme_settings.header_position_y}
name="header_position_y"
class="admin-range w-full"
/>
</form>
</div>
<% end %>
<%= for entry <- @uploads.header_upload.entries do %>
<div class="flex items-center gap-2 mt-2">
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
<div
class="h-full bg-primary transition-all"
style={"width: #{entry.progress}%"}
>
</div>
</div>
<span class="text-xs text-base-content/60">{entry.progress}%</span>
<button
type="button"
phx-click="cancel_upload"
phx-value-ref={entry.ref}
phx-value-upload="header_upload"
class="text-base-content/40 hover:text-base-content/70"
>
×
</button>
</div>
<%= for err <- upload_errors(@uploads.header_upload, entry) do %>
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %>
<% end %>
<%= for err <- upload_errors(@uploads.header_upload) do %>
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
<% end %>
</div>
<% end %>
<!-- Presets Section -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Start with a preset
</label>
<div class="grid grid-cols-2 gap-2">
<%= for {preset_name, description} <- @presets_with_descriptions do %>
<button
type="button"
phx-click="apply_preset"
phx-value-preset={preset_name}
class={[
"p-3 rounded-lg text-left transition-all border-2",
if(@active_preset == preset_name,
do: "border-base-content bg-base-100",
else: "border-transparent bg-base-200 hover:bg-base-300"
)
]}
>
<div class="font-semibold text-sm capitalize text-base-content">
{preset_name}
</div>
<div class="text-xs text-base-content/60">{description}</div>
</button>
<% end %>
</div>
</div>
<!-- Accent Colors (stays in essentials) -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Accent colour
</label>
<form
id="accent-color-form"
phx-change="update_color"
phx-value-field="accent_color"
phx-hook="ColorSync"
>
<div class="flex items-center gap-3">
<input
type="color"
id="accent-color-picker"
name="value"
value={@theme_settings.accent_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70">
{@theme_settings.accent_color}
</span>
</div>
</form>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Hover colour
</label>
<form
id="secondary-accent-color-form"
phx-change="update_color"
phx-value-field="secondary_accent_color"
phx-hook="ColorSync"
>
<div class="flex items-center gap-3">
<input
type="color"
id="secondary-accent-color-picker"
name="value"
value={@theme_settings.secondary_accent_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70">
{@theme_settings.secondary_accent_color}
</span>
</div>
</form>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Sale colour
</label>
<form
id="sale-color-form"
phx-change="update_color"
phx-value-field="sale_color"
phx-hook="ColorSync"
>
<div class="flex items-center gap-3">
<input
type="color"
id="sale-color-picker"
name="value"
value={@theme_settings.sale_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70">
{@theme_settings.sale_color}
</span>
</div>
</form>
</div>
<!-- Customise Section (collapsible accordion using native details/summary) -->
<details
class="border-t border-base-300 mt-6 pt-4 group"
id="customise-section"
open={@customise_open}
>
<summary
class="flex items-center justify-between w-full py-3 cursor-pointer list-none [&::-webkit-details-marker]:hidden"
phx-click="toggle_customise"
>
<span class="text-sm font-semibold text-base-content/70 group-hover:text-base-content transition-colors">
Customise
</span>
<svg
class="w-5 h-5 text-base-content/50 transition-transform group-open:rotate-180"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</summary>
<div class="pt-4">
<!-- Typography Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="4 7 4 4 20 4 20 7"></polyline>
<line x1="9" y1="20" x2="15" y2="20"></line>
<line x1="12" y1="4" x2="12" y2="20"></line>
</svg>
<span class="text-sm font-semibold text-base-content">Typography</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Font style
</label>
<div class="flex flex-wrap gap-2">
<%= for typo <- ["clean", "editorial", "modern", "classic", "friendly", "minimal"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="typography"
phx-value-setting_value={typo}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.typography == typo,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{typo}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Font size
</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="font_size"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.font_size == value,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{label}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Heading weight
</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="heading_weight"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.heading_weight == value,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{label}
</button>
<% end %>
</div>
</div>
</div>
<!-- Colours Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<span class="text-sm font-semibold text-base-content">Colours</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Colour mood
</label>
<div class="flex flex-wrap gap-2">
<%= for mood <- ["warm", "neutral", "cool", "dark"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="mood"
phx-value-setting_value={mood}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.mood == mood,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{mood}
</button>
<% end %>
</div>
</div>
</div>
<!-- Layout Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line>
<line x1="9" y1="21" x2="9" y2="9"></line>
</svg>
<span class="text-sm font-semibold text-base-content">Layout</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Product grid
</label>
<div class="flex flex-wrap gap-2">
<%= for cols <- ["2", "3", "4"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="grid_columns"
phx-value-setting_value={cols}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.grid_columns == cols,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{cols} columns
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Density
</label>
<div class="flex flex-wrap gap-2">
<%= for density <- ["spacious", "balanced", "compact"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="density"
phx-value-setting_value={density}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.density == density,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{density}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Header layout
</label>
<div class="flex flex-wrap gap-2">
<%= for layout <- ["standard", "centered", "left"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="header_layout"
phx-value-setting_value={layout}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.header_layout == layout,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{layout}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.announcement_bar}
phx-click="toggle_setting"
phx-value-field="announcement_bar"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="text-sm text-base-content">Announcement bar</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.sticky_header}
phx-click="toggle_setting"
phx-value-field="sticky_header"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="text-sm text-base-content">Sticky header</span>
</label>
</div>
</div>
<!-- Shape Group -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-4">
<svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
</svg>
<span class="text-sm font-semibold text-base-content">Shape</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Corner style
</label>
<div class="flex flex-wrap gap-2">
<%= for shape <- ["sharp", "soft", "round", "pill"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="shape"
phx-value-setting_value={shape}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.shape == shape,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{shape}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Card shadow
</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"none", "None"}, {"sm", "Subtle"}, {"md", "Medium"}, {"lg", "Strong"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="card_shadow"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.card_shadow == value,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{label}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Button style
</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="button_style"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.button_style == value,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{label}
</button>
<% end %>
</div>
</div>
</div>
<!-- Products Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
<span class="text-sm font-semibold text-base-content">Products</span>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Content width
</label>
<div class="flex flex-wrap gap-2">
<%= for width <- ["contained", "wide", "full"] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="layout_width"
phx-value-setting_value={width}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all capitalize",
if(@theme_settings.layout_width == width,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{width}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Image aspect ratio
</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="image_aspect_ratio"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.image_aspect_ratio == value,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{label}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">
Product text alignment
</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="product_text_align"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.product_text_align == value,
do: "border-base-content bg-base-100 text-base-content",
else:
"border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
{label}
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.hover_image}
phx-click="toggle_setting"
phx-value-field="hover_image"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="text-sm text-base-content">Second image on hover</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.show_prices}
phx-click="toggle_setting"
phx-value-field="show_prices"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="text-sm text-base-content">Show prices</span>
</label>
</div>
</div>
<!-- Product Page Group -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-4">
<svg
class="w-4 h-4 text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line>
</svg>
<span class="text-sm font-semibold text-base-content">Product page</span>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_trust_badges}
phx-click="toggle_setting"
phx-value-field="pdp_trust_badges"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="text-sm text-base-content">Trust badges</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_reviews}
phx-click="toggle_setting"
phx-value-field="pdp_reviews"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="text-sm text-base-content">Reviews section</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_related_products}
phx-click="toggle_setting"
phx-value-field="pdp_related_products"
class="admin-checkbox admin-checkbox-sm"
/>
<span class="text-sm text-base-content">Related products</span>
</label>
</div>
</div>
</div>
</details>
<!-- Current Combination Display -->
<div class="bg-base-200 rounded-xl p-4 mt-6">
<div class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-2">
Current combination
</div>
<div class="text-sm text-base-content leading-relaxed">
{String.capitalize(@theme_settings.mood)} · {String.capitalize(
@theme_settings.typography
)} · {String.capitalize(@theme_settings.shape)} · {String.capitalize(
@theme_settings.density
)} · {@theme_settings.grid_columns}-up · {String.capitalize(
@theme_settings.header_layout
)}
</div>
<div class="text-xs text-base-content/40 mt-2">
One of 100,000+ possible combinations
</div>
</div>
<% end %>
</div>
<!-- Preview Area -->
<div class="flex-1 p-6 flex flex-col overflow-hidden bg-base-200">
<div class="max-w-[1200px] mx-auto w-full flex flex-col flex-1 min-h-0 overflow-hidden">
<!-- Preview Page Switcher -->
<div class="flex gap-1 mb-3 bg-base-300 p-1 rounded-lg w-fit flex-shrink-0">
<%= for {page_name, label} <- [
{:home, "Home"},
{:collection, "Collection"},
{:pdp, "Product"},
{:cart, "Cart"},
{:about, "About"},
{:delivery, "Delivery"},
{:privacy, "Privacy"},
{:terms, "Terms"},
{:contact, "Contact"},
{:error, "404"}
] do %>
<button
type="button"
phx-click="change_preview_page"
phx-value-page={page_name}
class={[
"px-4 py-2 text-sm font-medium rounded transition-all",
if(@preview_page == page_name,
do: "bg-base-100 text-base-content shadow-sm",
else: "text-base-content/70 hover:text-base-content"
)
]}
>
{label}
</button>
<% end %>
</div>
<!-- Browser Chrome -->
<div class="flex items-center bg-gradient-to-b from-base-300 to-base-300/80 border border-base-content/20 border-b-base-content/30 rounded-t-[10px] px-[14px] py-[10px] gap-[14px] flex-shrink-0">
<div class="flex gap-2">
<div class="w-3 h-3 rounded-full bg-[#ff5f57] border border-[#e14640]"></div>
<div class="w-3 h-3 rounded-full bg-[#ffbd2e] border border-[#dfa123]"></div>
<div class="w-3 h-3 rounded-full bg-[#28c940] border border-[#1aab29]"></div>
</div>
<div class="flex-1 flex items-center gap-2 bg-base-100 border border-base-content/20 rounded-md px-3 py-[5px]">
<svg
class="w-[14px] h-[14px] text-base-content/50"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span class="text-sm text-base-content/60 truncate">
{@theme_settings.site_name |> String.downcase() |> String.replace(" ", "")}.myshopify.com
</span>
</div>
</div>
<!-- Preview Frame -->
<div
class="themed overflow-auto flex-1 rounded-b-lg border border-t-0 border-base-content/20"
data-mood={@theme_settings.mood}
data-typography={@theme_settings.typography}
data-shape={@theme_settings.shape}
data-density={@theme_settings.density}
data-grid={@theme_settings.grid_columns}
data-header={@theme_settings.header_layout}
data-sticky={to_string(@theme_settings.sticky_header)}
data-layout={@theme_settings.layout_width}
data-shadow={@theme_settings.card_shadow}
data-button-style={@theme_settings.button_style}
>
<style>
/* All font faces for theme switching */
<%= Phoenix.HTML.raw(Berrypod.Theme.Fonts.generate_all_font_faces(
&BerrypodWeb.Endpoint.static_path/1
)) %>
/* Generated theme CSS */
<%= Phoenix.HTML.raw(@generated_css) %>
</style>
<.preview_page
page={@preview_page}
preview_data={@preview_data}
theme_settings={@theme_settings}
logo_image={@logo_image}
header_image={@header_image}
cart_drawer_open={@cart_drawer_open}
/>
</div>
</div>
</div>
</div>
</div>