feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
<div class="min-h-screen bg-base-200">
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<div class="flex flex-col lg:flex-row lg:h-screen">
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
<!-- Controls Sidebar -->
|
2026-01-19 21:37:34 +00:00
|
|
|
|
<div
|
|
|
|
|
|
id="theme-sidebar"
|
|
|
|
|
|
class={[
|
|
|
|
|
|
"bg-base-100 border-r border-base-300 lg:h-screen flex-shrink-0 transition-all duration-300",
|
2026-01-31 14:24:58 +00:00
|
|
|
|
if(@sidebar_collapsed,
|
|
|
|
|
|
do: "w-12 overflow-hidden",
|
|
|
|
|
|
else: "w-full lg:w-[380px] overflow-y-auto p-6"
|
|
|
|
|
|
)
|
2026-01-19 21:37:34 +00:00
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 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"
|
|
|
|
|
|
>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
|
|
|
|
|
>
|
2026-01-19 21:37:34 +00:00
|
|
|
|
<polyline points="9 18 15 12 9 6"></polyline>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<% else %>
|
2026-02-12 08:35:22 +00:00
|
|
|
|
<.link
|
2026-02-12 14:17:38 +00:00
|
|
|
|
href={~p"/admin"}
|
2026-02-12 08:35:22 +00:00
|
|
|
|
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 -->
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2026-01-19 21:37:34 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 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} <- [
|
2025-12-31 18:55:44 +00:00
|
|
|
|
{"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 %>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<label class={[
|
2025-12-31 18:55:44 +00:00
|
|
|
|
"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"
|
|
|
|
|
|
)
|
|
|
|
|
|
]}>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<span class={[
|
2026-01-31 14:24:58 +00:00
|
|
|
|
"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)
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</span>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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
|
2026-02-16 17:47:41 +00:00
|
|
|
|
src={"/image_cache/#{@logo_image.id}.webp"}
|
2026-01-31 14:24:58 +00:00
|
|
|
|
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 %>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<%= 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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<%= 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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-range w-full"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</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"
|
|
|
|
|
|
}
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-toggle admin-toggle-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<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 %>
|
2025-12-31 00:24:53 +00:00
|
|
|
|
<% end %>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<% end %>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
add favicon and site icon generation from uploaded images
Upload a source image (PNG, JPEG, or SVG) and get a complete favicon
setup: PNG variants at 32, 180, 192, 512px served from DB via
FaviconController with ETag caching, SVG favicon for vector sources,
dynamic site.webmanifest, and theme-color meta tag. Theme editor gains
a site icon section with "use logo as icon" toggle, dedicated icon
upload, short name, and background colour picker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:22:15 +00:00
|
|
|
|
<!-- 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>
|
|
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<!-- 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"
|
|
|
|
|
|
}
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-toggle admin-toggle-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<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
|
2026-02-16 17:47:41 +00:00
|
|
|
|
src={"/image_cache/#{@header_image.id}.webp"}
|
2026-01-31 14:24:58 +00:00
|
|
|
|
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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-range w-full"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-range w-full"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-range w-full"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<% end %>
|
2025-12-31 00:24:53 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<%= for entry <- @uploads.header_upload.entries do %>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<div class="flex items-center gap-2 mt-2">
|
|
|
|
|
|
<div class="flex-1 h-1.5 bg-base-300 rounded-full overflow-hidden">
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<div
|
|
|
|
|
|
class="h-full bg-primary transition-all"
|
|
|
|
|
|
style={"width: #{entry.progress}%"}
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<span class="text-xs text-base-content/60">{entry.progress}%</span>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
phx-click="cancel_upload"
|
|
|
|
|
|
phx-value-ref={entry.ref}
|
2026-01-31 14:24:58 +00:00
|
|
|
|
phx-value-upload="header_upload"
|
2025-12-31 18:55:44 +00:00
|
|
|
|
class="text-base-content/40 hover:text-base-content/70"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<%= for err <- upload_errors(@uploads.header_upload, entry) do %>
|
|
|
|
|
|
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<% end %>
|
2025-12-31 00:24:53 +00:00
|
|
|
|
<% end %>
|
|
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<%= for err <- upload_errors(@uploads.header_upload) do %>
|
|
|
|
|
|
<p class="text-error text-xs mt-1">{error_to_string(err)}</p>
|
2025-12-31 00:24:53 +00:00
|
|
|
|
<% end %>
|
|
|
|
|
|
</div>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<% end %>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 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 %>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
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}
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
</form>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 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>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 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>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-checkbox admin-checkbox-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm text-base-content">Announcement bar</span>
|
|
|
|
|
|
</label>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-checkbox admin-checkbox-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm text-base-content">Sticky header</span>
|
|
|
|
|
|
</label>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Shape Group -->
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<div class="mb-4">
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-01 16:16:05 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2026-01-01 16:16:05 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- 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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</div>
|
2026-01-01 16:16:05 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2026-01-01 16:16:05 +00:00
|
|
|
|
</div>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
</div>
|
2026-01-01 16:16:05 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-checkbox admin-checkbox-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm text-base-content">Second image on hover</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2026-01-01 16:16:05 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-checkbox admin-checkbox-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm text-base-content">Show prices</span>
|
|
|
|
|
|
</label>
|
2026-01-01 16:16:05 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Product Page Group -->
|
2026-01-02 13:48:03 +00:00
|
|
|
|
<div class="mb-4">
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-checkbox admin-checkbox-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm text-base-content">Trust badges</span>
|
|
|
|
|
|
</label>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-checkbox admin-checkbox-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm text-base-content">Reviews section</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<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"
|
2026-02-17 23:05:01 +00:00
|
|
|
|
class="admin-checkbox admin-checkbox-sm"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
/>
|
|
|
|
|
|
<span class="text-sm text-base-content">Related products</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
2026-01-02 13:48:03 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
</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
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-19 21:37:34 +00:00
|
|
|
|
<% end %>
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Preview Area -->
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<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">
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
<!-- Preview Page Switcher -->
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<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"},
|
2026-02-08 10:47:54 +00:00
|
|
|
|
{:delivery, "Delivery"},
|
|
|
|
|
|
{:privacy, "Privacy"},
|
|
|
|
|
|
{:terms, "Terms"},
|
2025-12-31 18:55:44 +00:00
|
|
|
|
{: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"
|
|
|
|
|
|
)
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
{label}
|
2025-12-31 18:55:44 +00:00
|
|
|
|
</button>
|
|
|
|
|
|
<% end %>
|
|
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Browser Chrome -->
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<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]">
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<svg
|
|
|
|
|
|
class="w-[14px] h-[14px] text-base-content/50"
|
|
|
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
|
stroke-width="2"
|
|
|
|
|
|
>
|
2025-12-31 18:55:44 +00:00
|
|
|
|
<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>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
<span class="text-sm text-base-content/60 truncate">
|
|
|
|
|
|
{@theme_settings.site_name |> String.downcase() |> String.replace(" ", "")}.myshopify.com
|
|
|
|
|
|
</span>
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-31 14:24:58 +00:00
|
|
|
|
|
|
|
|
|
|
<!-- Preview Frame -->
|
|
|
|
|
|
<div
|
2026-02-21 00:13:33 +00:00
|
|
|
|
class="themed overflow-auto flex-1 rounded-b-lg border border-t-0 border-base-content/20"
|
2026-01-31 14:24:58 +00:00
|
|
|
|
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}
|
|
|
|
|
|
>
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
<style>
|
2026-01-25 11:36:20 +00:00
|
|
|
|
/* All font faces for theme switching */
|
2026-02-18 21:23:15 +00:00
|
|
|
|
<%= Phoenix.HTML.raw(Berrypod.Theme.Fonts.generate_all_font_faces(
|
|
|
|
|
|
&BerrypodWeb.Endpoint.static_path/1
|
2026-01-25 11:36:20 +00:00
|
|
|
|
)) %>
|
|
|
|
|
|
/* Generated theme CSS */
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
<%= Phoenix.HTML.raw(@generated_css) %>
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
2026-01-17 22:17:59 +00:00
|
|
|
|
<.preview_page
|
|
|
|
|
|
page={@preview_page}
|
|
|
|
|
|
preview_data={@preview_data}
|
|
|
|
|
|
theme_settings={@theme_settings}
|
|
|
|
|
|
logo_image={@logo_image}
|
|
|
|
|
|
header_image={@header_image}
|
2026-02-05 22:11:16 +00:00
|
|
|
|
cart_drawer_open={@cart_drawer_open}
|
2026-01-17 22:17:59 +00:00
|
|
|
|
/>
|
feat: add Theme LiveView with preset switching
Implement basic theme editor interface with live preview:
- ThemeLive.Index LiveView with mount and event handlers
- Two-column layout: controls sidebar + preview area
- Display all 9 presets as clickable buttons
- Apply preset and regenerate CSS on click
- Show current theme settings (mood, typography, shape, density, color)
- Preview page switcher (7 pages: home, collection, product, cart, about, contact, 404)
- Inline <style> tag with generated CSS for instant preview
- Basic preview frame showing theme variables in action
- Authentication required via :require_authenticated_user pipeline
- Theme navigation link added to user menu
- 9 comprehensive LiveView tests
All tests passing (197 total).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 21:53:52 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|