All modules, configs, paths, and references updated. 836 tests pass, zero warnings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1184 lines
49 KiB
Plaintext
1184 lines
49 KiB
Plaintext
<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>
|
||
<% 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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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 preview-frame bg-white 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>
|