defmodule SimpleshopThemeWeb.ShopComponents do @moduledoc """ Provides shop/storefront UI components. These components are shared between the theme preview system and the public storefront pages. They render using CSS custom properties defined by the theme settings. """ use Phoenix.Component @doc """ Renders the announcement bar. The bar displays promotional messaging at the top of the page. It uses CSS custom properties for theming. ## Attributes * `theme_settings` - Required. The theme settings map. * `message` - Optional. The announcement message to display. Defaults to "Free delivery on orders over £40". ## Examples <.announcement_bar theme_settings={@theme_settings} /> <.announcement_bar theme_settings={@theme_settings} message="20% off this weekend!" /> """ attr :theme_settings, :map, required: true attr :message, :string, default: "Free delivery on orders over £40" def announcement_bar(assigns) do ~H"""

{@message}

""" end @doc """ Renders the skip link for keyboard navigation accessibility. This is a standard accessibility pattern that allows keyboard users to skip directly to the main content. """ def skip_link(assigns) do ~H""" """ end # ============================================================================= # Themed Form Components # ============================================================================= # These components wrap common form elements with the themed CSS classes. # They provide a consistent, theme-aware appearance across shop pages. @doc """ Renders a themed text input. This component applies the `.themed-input` CSS class which inherits colors, borders, and radii from the current theme's CSS variables. ## Attributes * `type` - Optional. Input type. Defaults to "text". * `class` - Optional. Additional CSS classes. * All other attributes are passed through to the input element. ## Examples <.shop_input type="email" placeholder="your@email.com" /> <.shop_input type="text" name="name" class="flex-1" /> """ attr :type, :string, default: "text" attr :class, :string, default: nil attr :rest, :global, include: ~w(name value placeholder required disabled autocomplete readonly) def shop_input(assigns) do ~H""" """ end @doc """ Renders a themed textarea. ## Attributes * `class` - Optional. Additional CSS classes. * All other attributes are passed through to the textarea element. ## Examples <.shop_textarea placeholder="Your message..." rows="5" /> """ attr :class, :string, default: nil attr :rest, :global, include: ~w(name rows placeholder required disabled readonly) def shop_textarea(assigns) do ~H""" """ end @doc """ Renders a themed select dropdown. ## Attributes * `class` - Optional. Additional CSS classes. * `options` - Required. List of options (strings or {value, label} tuples). * `selected` - Optional. Currently selected value. * All other attributes are passed through to the select element. ## Examples <.shop_select options={["Option 1", "Option 2"]} /> <.shop_select options={[{"value", "Label"}]} selected="value" /> """ attr :class, :string, default: nil attr :options, :list, required: true attr :selected, :any, default: nil attr :rest, :global, include: ~w(name required disabled aria-label) def shop_select(assigns) do ~H""" """ end @doc """ Renders a themed primary button (accent color background). ## Attributes * `class` - Optional. Additional CSS classes. * `type` - Optional. Button type. Defaults to "button". * All other attributes are passed through to the button element. ## Slots * `inner_block` - Required. Button content. ## Examples <.shop_button>Send Message <.shop_button type="submit" class="w-full">Subscribe """ attr :class, :string, default: nil attr :type, :string, default: "button" attr :rest, :global, include: ~w(disabled name value phx-click phx-value-page) slot :inner_block, required: true def shop_button(assigns) do ~H""" """ end @doc """ Renders a themed outline/secondary button. ## Attributes * `class` - Optional. Additional CSS classes. * `type` - Optional. Button type. Defaults to "button". * All other attributes are passed through to the button element. ## Slots * `inner_block` - Required. Button content. ## Examples <.shop_button_outline>Continue Shopping """ attr :class, :string, default: nil attr :type, :string, default: "button" attr :rest, :global, include: ~w(disabled name value phx-click phx-value-page) slot :inner_block, required: true def shop_button_outline(assigns) do ~H""" """ end @doc """ Renders a themed link styled as a primary button. ## Attributes * `href` - Required. Link destination. * `class` - Optional. Additional CSS classes. ## Slots * `inner_block` - Required. Link content. ## Examples <.shop_link_button href="/checkout">Checkout """ attr :href, :string, required: true attr :class, :string, default: nil slot :inner_block, required: true def shop_link_button(assigns) do ~H""" {render_slot(@inner_block)} """ end @doc """ Renders a themed link styled as an outline button. ## Attributes * `href` - Required. Link destination. * `class` - Optional. Additional CSS classes. ## Slots * `inner_block` - Required. Link content. ## Examples <.shop_link_outline href="/collections/all">Continue Shopping """ attr :href, :string, required: true attr :class, :string, default: nil slot :inner_block, required: true def shop_link_outline(assigns) do ~H""" {render_slot(@inner_block)} """ end @doc """ Renders a themed card container. ## Attributes * `class` - Optional. Additional CSS classes. ## Slots * `inner_block` - Required. Card content. ## Examples <.shop_card class="p-6">

Card Title

Card content...

""" attr :class, :string, default: nil slot :inner_block, required: true def shop_card(assigns) do ~H"""
{render_slot(@inner_block)}
""" end @doc """ Renders a mobile bottom navigation bar. This component provides thumb-friendly navigation for mobile devices, following modern UX best practices. It's hidden on larger screens where the standard header navigation is used. ## Attributes * `active_page` - Required. The current page identifier (e.g., "home", "collection", "about", "contact"). * `mode` - Optional. Either `:live` (default) for real navigation or `:preview` for theme preview mode with phx-click handlers. * `cart_count` - Optional. Number of items in cart for badge display. Default: 0. ## Examples <.mobile_bottom_nav active_page="home" /> <.mobile_bottom_nav active_page="collection" mode={:preview} /> """ attr :active_page, :string, required: true attr :mode, :atom, default: :live attr :cart_count, :integer, default: 0 def mobile_bottom_nav(assigns) do ~H""" """ end attr :icon, :atom, required: true attr :label, :string, required: true attr :page, :string, required: true attr :href, :string, required: true attr :active_page, :string, required: true attr :active_pages, :list, default: nil attr :mode, :atom, default: :live defp mobile_nav_item(assigns) do active_pages = assigns.active_pages || [assigns.page] is_current = assigns.active_page in active_pages assigns = assign(assigns, :is_current, is_current) ~H"""
  • <%= if @mode == :preview do %> <.nav_icon icon={@icon} size={if @is_current, do: "w-6 h-6", else: "w-5 h-5"} /> {@label} <% else %> <.nav_icon icon={@icon} size={if @is_current, do: "w-6 h-6", else: "w-5 h-5"} /> {@label} <% end %>
  • """ end defp nav_icon(%{icon: :home} = assigns) do assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end) ~H""" """ end defp nav_icon(%{icon: :shop} = assigns) do assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end) ~H""" """ end defp nav_icon(%{icon: :about} = assigns) do assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end) ~H""" """ end defp nav_icon(%{icon: :contact} = assigns) do assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end) ~H""" """ end @doc """ Renders the search modal overlay. This is a modal dialog for searching products. Currently provides the UI shell; search functionality will be added later. ## Attributes * `hint_text` - Optional. Hint text shown below the search input. Defaults to nil (no hint shown). ## Examples <.search_modal /> <.search_modal hint_text="Try searching for \"mountain\" or \"forest\"" /> """ attr :hint_text, :string, default: nil def search_modal(assigns) do ~H""" """ end @doc """ Renders the shop footer with newsletter signup and links. ## Attributes * `theme_settings` - Required. The theme settings map containing site_name. * `mode` - Optional. Either `:live` (default) for real navigation or `:preview` for theme preview mode with phx-click handlers. ## Examples <.shop_footer theme_settings={@theme_settings} /> <.shop_footer theme_settings={@theme_settings} mode={:preview} /> """ attr :theme_settings, :map, required: true attr :mode, :atom, default: :live def shop_footer(assigns) do assigns = assign(assigns, :current_year, Date.utc_today().year) ~H""" """ end @doc """ Renders the shop header with logo, navigation, and actions. ## Attributes * `theme_settings` - Required. The theme settings map. * `logo_image` - Optional. The logo image struct (with id, is_svg fields). * `header_image` - Optional. The header background image struct. * `active_page` - Optional. Current page for nav highlighting. * `mode` - Optional. Either `:live` (default) or `:preview`. * `cart_count` - Optional. Number of items in cart. Defaults to 0. ## Examples <.shop_header theme_settings={@theme_settings} /> <.shop_header theme_settings={@theme_settings} mode={:preview} cart_count={2} /> """ attr :theme_settings, :map, required: true attr :logo_image, :map, default: nil attr :header_image, :map, default: nil attr :active_page, :string, default: nil attr :mode, :atom, default: :live attr :cart_count, :integer, default: 0 def shop_header(assigns) do ~H"""
    <%= if @theme_settings.header_background_enabled && @header_image do %>
    <% end %>
    """ end defp logo_url(logo_image, %{logo_recolor: true, logo_color: color}) when logo_image.is_svg do clean_color = String.trim_leading(color, "#") "/images/#{logo_image.id}/recolored/#{clean_color}" end defp logo_url(logo_image, _), do: "/images/#{logo_image.id}" # Logo content that links to home, except when already on home page. # This follows accessibility best practices - current page should not be a link. attr :theme_settings, :map, required: true attr :logo_image, :map, default: nil attr :active_page, :string, default: nil attr :mode, :atom, default: :live defp logo_content(assigns) do is_home = assigns.active_page == "home" assigns = assign(assigns, :is_home, is_home) ~H""" <%= if @is_home do %> <.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} /> <% else %> <%= if @mode == :preview do %> <.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} /> <% else %> <.logo_inner theme_settings={@theme_settings} logo_image={@logo_image} /> <% end %> <% end %> """ end attr :theme_settings, :map, required: true attr :logo_image, :map, default: nil defp logo_inner(assigns) do ~H""" <%= case @theme_settings.logo_mode do %> <% "text-only" -> %> {@theme_settings.site_name} <% "logo-text" -> %> <%= if @logo_image do %> {@theme_settings.site_name} <% end %> {@theme_settings.site_name} <% "logo-only" -> %> <%= if @logo_image do %> {@theme_settings.site_name} <% else %> {@theme_settings.site_name} <% end %> <% _ -> %> {@theme_settings.site_name} <% end %> """ end defp header_background_style(settings, header_image) do "position: absolute; top: 0; left: 0; right: 0; bottom: 0; " <> "background-image: url('/images/#{header_image.id}'); " <> "background-size: #{settings.header_zoom}%; " <> "background-position: #{settings.header_position_x}% #{settings.header_position_y}%; " <> "background-repeat: no-repeat; z-index: 0;" end # Navigation item that renders as a span (not a link) when on the current page. # This follows accessibility best practices - current page should not be a link. attr :label, :string, required: true attr :page, :string, required: true attr :active_page, :string, required: true attr :href, :string, default: nil attr :mode, :atom, default: :live attr :active_pages, :list, default: nil defp nav_item(assigns) do # Allow matching multiple pages (e.g., "Shop" is active for both collection and pdp) active_pages = assigns.active_pages || [assigns.page] is_current = assigns.active_page in active_pages assigns = assign(assigns, :is_current, is_current) ~H""" <%= if @is_current do %> {@label} <% else %> <%= if @mode == :preview do %> {@label} <% else %> {@label} <% end %> <% end %> """ end @doc """ Renders the cart drawer (floating sidebar). The drawer slides in from the right when opened. It displays cart items and checkout options. ## Attributes * `cart_items` - List of cart items to display. Each item should have `image`, `name`, `variant`, and `price` keys. Default: [] * `subtotal` - The subtotal to display. Default: nil (shows "£0.00") * `mode` - Either `:live` (default) for real stores or `:preview` for theme editor. In preview mode, "View basket" navigates via LiveView JS commands. ## Examples <.cart_drawer cart_items={@cart.items} subtotal={@cart.subtotal} /> <.cart_drawer cart_items={demo_items} subtotal="£72.00" mode={:preview} /> """ attr :cart_items, :list, default: [] attr :subtotal, :string, default: nil attr :mode, :atom, default: :live def cart_drawer(assigns) do assigns = assign_new(assigns, :display_subtotal, fn -> assigns.subtotal || "£0.00" end) ~H"""

    Your basket

    <%= for item <- @cart_items do %>

    {item.name}

    {item.variant}

    {item.price}

    <% end %>
    """ end @doc """ Renders a product card with configurable variants. ## Attributes * `product` - Required. The product map with `name`, `image_url`, `price`, etc. * `theme_settings` - Required. The theme settings map. * `mode` - Either `:live` (default) or `:preview`. * `variant` - The visual variant: - `:default` - Collection page style with border, category, full details - `:featured` - Home page style with hover lift, no border - `:compact` - PDP related products with aspect-square, minimal info - `:minimal` - Error 404 style, smallest, not clickable * `show_category` - Show category label. Defaults based on variant. * `show_badges` - Show product badges. Defaults based on variant. * `show_delivery_text` - Show "Free delivery" text. Defaults based on variant. * `clickable` - Whether the card navigates. Defaults based on variant. ## Examples <.product_card product={product} theme_settings={@theme_settings} /> <.product_card product={product} theme_settings={@theme_settings} variant={:featured} mode={:preview} /> """ attr :product, :map, required: true attr :theme_settings, :map, required: true attr :mode, :atom, default: :live attr :variant, :atom, default: :default attr :show_category, :boolean, default: nil attr :show_badges, :boolean, default: nil attr :show_delivery_text, :boolean, default: nil attr :clickable, :boolean, default: nil def product_card(assigns) do # Apply variant defaults for nil values defaults = variant_defaults(assigns.variant) assigns = assigns |> assign_new(:show_category_resolved, fn -> if assigns.show_category == nil, do: defaults.show_category, else: assigns.show_category end) |> assign_new(:show_badges_resolved, fn -> if assigns.show_badges == nil, do: defaults.show_badges, else: assigns.show_badges end) |> assign_new(:show_delivery_text_resolved, fn -> if assigns.show_delivery_text == nil, do: defaults.show_delivery_text, else: assigns.show_delivery_text end) |> assign_new(:clickable_resolved, fn -> if assigns.clickable == nil, do: defaults.clickable, else: assigns.clickable end) ~H""" <%= if @clickable_resolved do %> <%= if @mode == :preview do %> <.product_card_inner product={@product} theme_settings={@theme_settings} variant={@variant} show_category={@show_category_resolved} show_badges={@show_badges_resolved} show_delivery_text={@show_delivery_text_resolved} /> <% else %> <.product_card_inner product={@product} theme_settings={@theme_settings} variant={@variant} show_category={@show_category_resolved} show_badges={@show_badges_resolved} show_delivery_text={@show_delivery_text_resolved} /> <% end %> <% else %>
    <.product_card_inner product={@product} theme_settings={@theme_settings} variant={@variant} show_category={@show_category_resolved} show_badges={@show_badges_resolved} show_delivery_text={@show_delivery_text_resolved} />
    <% end %> """ end attr :product, :map, required: true attr :theme_settings, :map, required: true attr :variant, :atom, required: true attr :show_category, :boolean, required: true attr :show_badges, :boolean, required: true attr :show_delivery_text, :boolean, required: true defp product_card_inner(assigns) do ~H"""
    <%= if @show_badges do %> <.product_badge product={@product} /> <% end %> <.product_card_image product={@product} variant={@variant} class="product-image-primary w-full h-full object-cover transition-opacity duration-300" /> <%= if @theme_settings.hover_image && @product[:hover_image_url] do %> <.product_card_image product={@product} variant={@variant} image_key={:hover} class="product-image-hover w-full h-full object-cover" /> <% end %>
    <%= if @show_category && @product[:category] do %>

    {@product.category}

    <% end %>

    {@product.name}

    <%= if @theme_settings.show_prices do %> <.product_price product={@product} variant={@variant} /> <% end %> <%= if @show_delivery_text do %>

    Free delivery over £40

    <% end %>
    """ end # Helper to render product images with responsive variants. # Works for both mockups (static files) and database images. # Requires source_width to be set for responsive image support. attr :product, :map, required: true attr :variant, :atom, required: true attr :class, :string, default: "" attr :image_key, :atom, default: :primary defp product_card_image(assigns) do # Determine which image fields to use based on primary vs hover {image_id_field, image_url_field, source_width_field} = case assigns.image_key do :hover -> {:hover_image_id, :hover_image_url, :hover_source_width} _ -> {:image_id, :image_url, :source_width} end # Build the base image path: # - Database images: /images/{id}/variant # - Mockup images: {image_url} (e.g., /mockups/product-1) image_id = assigns.product[image_id_field] image_url = assigns.product[image_url_field] src = if image_id do "/images/#{image_id}/variant" else image_url end assigns = assigns |> assign(:src, src) |> assign(:source_width, assigns.product[source_width_field]) ~H""" <%= if @source_width do %> <.responsive_image src={@src} alt={@product.name} source_width={@source_width} sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px" class={@class} width={600} height={600} priority={@variant == :minimal} /> <% else %> {@product.name} <% end %> """ end attr :product, :map, required: true defp product_badge(assigns) do ~H""" <%= cond do %> <% Map.get(@product, :in_stock, true) == false -> %> Sold out <% @product[:is_new] -> %> New <% @product[:on_sale] -> %> Sale <% true -> %> <% end %> """ end attr :product, :map, required: true attr :variant, :atom, required: true defp product_price(assigns) do ~H""" <%= case @variant do %> <% :default -> %>
    <%= if @product.on_sale do %> £{@product.price / 100} £{@product.compare_at_price / 100} <% else %> £{@product.price / 100} <% end %>
    <% :featured -> %>

    <%= if @product.on_sale do %> £{@product.compare_at_price / 100} <% end %> £{@product.price / 100}

    <% :compact -> %>

    £{@product.price / 100}

    <% :minimal -> %>

    £{@product.price / 100}

    <% end %> """ end defp variant_defaults(:default), do: %{show_category: true, show_badges: true, show_delivery_text: true, clickable: true} defp variant_defaults(:featured), do: %{show_category: false, show_badges: true, show_delivery_text: true, clickable: true} defp variant_defaults(:compact), do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: true} defp variant_defaults(:minimal), do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: false} defp card_classes(:default), do: "product-card group overflow-hidden transition-all" defp card_classes(:featured), do: "product-card group overflow-hidden transition-all hover:-translate-y-1" defp card_classes(:compact), do: "product-card group overflow-hidden cursor-pointer" defp card_classes(:minimal), do: "product-card group overflow-hidden" defp card_style(:default), do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;" defp card_style(:featured), do: "background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;" defp card_style(:compact), do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);" defp card_style(:minimal), do: "background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);" defp image_container_classes(:compact), do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative" defp image_container_classes(:minimal), do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative" defp image_container_classes(_), do: "product-image-container bg-gray-200 overflow-hidden relative" defp content_padding_class(:compact), do: "p-3" defp content_padding_class(:minimal), do: "p-2" defp content_padding_class(_), do: "" defp title_classes(:default), do: "font-semibold mb-2" defp title_classes(:featured), do: "text-sm font-medium mb-1" defp title_classes(:compact), do: "font-semibold text-sm mb-1" defp title_classes(:minimal), do: "text-xs font-semibold truncate" defp title_style(:default), do: "font-family: var(--t-font-heading); color: var(--t-text-primary);" defp title_style(:featured), do: "color: var(--t-text-primary);" defp title_style(:compact), do: "font-family: var(--t-font-heading); color: var(--t-text-primary);" defp title_style(:minimal), do: "color: var(--t-text-primary);" @doc """ Renders a responsive product grid container. This component wraps product cards in a responsive grid layout. It supports theme-based column settings or fixed column layouts for specific use cases. ## Attributes * `theme_settings` - Optional. When provided, uses `grid_columns` setting for lg breakpoint. * `columns` - Optional. Fixed column count for lg breakpoint (overrides theme_settings). Use `:fixed_4` for a fixed 2/4 column layout (error page, related products). * `gap` - Optional. Gap size. Defaults to standard gap. * `class` - Optional. Additional CSS classes to add. ## Slots * `inner_block` - Required. The product cards to render inside the grid. ## Examples <.product_grid theme_settings={@theme_settings}> <%= for product <- @products do %> <.product_card product={product} theme_settings={@theme_settings} /> <% end %> <.product_grid columns={:fixed_4} gap="gap-6"> ... """ attr :theme_settings, :map, default: nil attr :columns, :atom, default: nil attr :gap, :string, default: nil attr :class, :string, default: nil slot :inner_block, required: true def product_grid(assigns) do ~H"""
    {render_slot(@inner_block)}
    """ end defp grid_classes(theme_settings, columns, gap, extra_class) do base = "product-grid grid" cols = cond do columns == :fixed_4 -> "grid-cols-2 md:grid-cols-4" theme_settings != nil -> responsive_cols = "grid-cols-1 sm:grid-cols-2" lg_cols = case theme_settings.grid_columns do "2" -> "lg:grid-cols-2" "3" -> "lg:grid-cols-3" "4" -> "lg:grid-cols-4" _ -> "lg:grid-cols-3" end "#{responsive_cols} #{lg_cols}" true -> "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" end gap_class = gap || "" [base, cols, gap_class, extra_class] |> Enum.reject(&is_nil/1) |> Enum.reject(&(&1 == "")) |> Enum.join(" ") end @doc """ Renders a centered hero section with title, description, and optional CTAs. ## Attributes * `title` - Required. The main heading text. * `description` - Required. The description paragraph text. * `variant` - The visual variant: - `:default` - Standard hero with section wrapper and padding - `:page` - Page header style, larger title, more spacing, no section wrapper - `:error` - Error page with pre-title (404), two buttons * `background` - Background style. Either `:base` (default) or `:sunken`. * `pre_title` - Optional. Text shown above title (e.g., "404" for error pages). * `cta_text` - Optional. Text for the primary CTA button. * `cta_page` - Optional. Page to navigate to on click (for preview mode). * `cta_href` - Optional. URL for live mode navigation. * `secondary_cta_text` - Optional. Text for secondary button (error variant). * `secondary_cta_page` - Optional. Page for secondary button (preview mode). * `secondary_cta_href` - Optional. URL for secondary button (live mode). * `mode` - Either `:live` (default) or `:preview`. ## Examples <.hero_section title="Original designs, printed on demand" description="From art prints to apparel..." cta_text="Shop the collection" cta_page="collection" mode={:preview} /> <.hero_section variant={:page} title="Get in touch" description="Questions about your order?" /> <.hero_section variant={:error} pre_title="404" title="Page Not Found" description="Sorry, that page doesn't exist..." cta_text="Go to Homepage" secondary_cta_text="Browse Products" mode={:preview} /> """ attr :title, :string, required: true attr :description, :string, required: true attr :variant, :atom, default: :default attr :background, :atom, default: :base attr :pre_title, :string, default: nil attr :cta_text, :string, default: nil attr :cta_page, :string, default: nil attr :cta_href, :string, default: nil attr :secondary_cta_text, :string, default: nil attr :secondary_cta_page, :string, default: nil attr :secondary_cta_href, :string, default: nil attr :mode, :atom, default: :live def hero_section(assigns) do ~H""" <%= case @variant do %> <% :default -> %>

    {@title}

    {@description}

    <.hero_cta :if={@cta_text} text={@cta_text} page={@cta_page} href={@cta_href} mode={@mode} variant={:primary} />
    <% :page -> %>

    {@title}

    {@description}

    <% :error -> %>
    <%= if @pre_title do %>

    {@pre_title}

    <% end %>

    {@title}

    {@description}

    <%= if @cta_text || @secondary_cta_text do %>
    <.hero_cta :if={@cta_text} text={@cta_text} page={@cta_page} href={@cta_href} mode={@mode} variant={:primary} /> <.hero_cta :if={@secondary_cta_text} text={@secondary_cta_text} page={@secondary_cta_page} href={@secondary_cta_href} mode={@mode} variant={:secondary} />
    <% end %>
    <% end %> """ end attr :text, :string, required: true attr :page, :string, default: nil attr :href, :string, default: nil attr :mode, :atom, required: true attr :variant, :atom, required: true defp hero_cta(assigns) do ~H""" <%= if @mode == :preview do %> <% else %> " text-decoration: none;"} > {@text} <% end %> """ end defp hero_cta_classes(:primary), do: "themed-button px-8 py-3 font-semibold transition-all" defp hero_cta_classes(:secondary), do: "themed-button-outline px-8 py-3 font-semibold transition-all" defp hero_cta_style(:primary), do: "" defp hero_cta_style(:secondary), do: "" @doc """ Renders a row of category circles for navigation. ## Attributes * `categories` - Required. List of category maps with `name` and `image_url`. * `limit` - Optional. Maximum number of categories to show. Defaults to 3. * `mode` - Either `:live` (default) or `:preview`. ## Examples <.category_nav categories={@categories} mode={:preview} /> <.category_nav categories={@categories} limit={4} /> """ attr :categories, :list, required: true attr :limit, :integer, default: 3 attr :mode, :atom, default: :live def category_nav(assigns) do ~H"""

    Shop by Category

    """ end @doc """ Renders a featured products section with title, product grid, and view-all button. ## Attributes * `title` - Required. Section heading text. * `products` - Required. List of products to display. * `theme_settings` - Required. The theme settings map. * `limit` - Optional. Maximum products to show. Defaults to 4. * `mode` - Either `:live` (default) or `:preview`. * `cta_text` - Optional. Text for the "view all" button. Defaults to "View all products". * `cta_page` - Optional. Page to navigate to (preview mode). Defaults to "collection". * `cta_href` - Optional. URL for live mode. Defaults to "/collections/all". ## Examples <.featured_products_section title="Featured products" products={@products} theme_settings={@theme_settings} mode={:preview} /> """ attr :title, :string, required: true attr :products, :list, required: true attr :theme_settings, :map, required: true attr :limit, :integer, default: 4 attr :mode, :atom, default: :live attr :cta_text, :string, default: "View all products" attr :cta_page, :string, default: "collection" attr :cta_href, :string, default: "/collections/all" def featured_products_section(assigns) do ~H"""

    {@title}

    <.product_grid theme_settings={@theme_settings}> <%= for product <- Enum.take(@products, @limit) do %> <.product_card product={product} theme_settings={@theme_settings} mode={@mode} variant={:featured} /> <% end %>
    <%= if @mode == :preview do %> <% else %> {@cta_text} <% end %>
    """ end @doc """ Renders a two-column image + text section. ## Attributes * `title` - Required. Section heading text. * `description` - Required. Body text content. * `image_url` - Required. URL for the image. * `link_text` - Optional. Text for the link. If nil, no link is shown. * `link_page` - Optional. Page to navigate to (preview mode). * `link_href` - Optional. URL for live mode. * `image_position` - Either `:left` (default) or `:right`. * `mode` - Either `:live` (default) or `:preview`. ## Examples <.image_text_section title="Made with passion, printed with care" description="Every design starts with an idea..." image_url="/mockups/example.jpg" link_text="Learn more about the studio →" link_page="about" mode={:preview} /> """ attr :title, :string, required: true attr :description, :string, required: true attr :image_url, :string, required: true attr :link_text, :string, default: nil attr :link_page, :string, default: nil attr :link_href, :string, default: nil attr :image_position, :atom, default: :left attr :mode, :atom, default: :live def image_text_section(assigns) do ~H"""
    <%= if @image_position == :left do %> <.image_text_image image_url={@image_url} /> <.image_text_content title={@title} description={@description} link_text={@link_text} link_page={@link_page} link_href={@link_href} mode={@mode} /> <% else %> <.image_text_content title={@title} description={@description} link_text={@link_text} link_page={@link_page} link_href={@link_href} mode={@mode} /> <.image_text_image image_url={@image_url} /> <% end %>
    """ end attr :image_url, :string, required: true defp image_text_image(assigns) do ~H"""
    """ end attr :title, :string, required: true attr :description, :string, required: true attr :link_text, :string, default: nil attr :link_page, :string, default: nil attr :link_href, :string, default: nil attr :mode, :atom, required: true defp image_text_content(assigns) do ~H"""

    {@title}

    {@description}

    <%= if @link_text do %> <%= if @mode == :preview do %> {@link_text} <% else %> {@link_text} <% end %> <% end %>
    """ end @doc """ Renders a page header with title and optional product count. ## Attributes * `title` - Required. The page title. * `subtitle` - Optional. Text below the title. * `product_count` - Optional. Number to display as "X products". ## Examples <.collection_header title="All Products" product_count={24} /> """ attr :title, :string, required: true attr :subtitle, :string, default: nil attr :product_count, :integer, default: nil def collection_header(assigns) do ~H"""

    {@title}

    <%= if @subtitle do %>

    {@subtitle}

    <% end %> <%= if @product_count do %>

    {@product_count} products

    <% end %>
    """ end @doc """ Renders a filter bar with category pills and sort dropdown. ## Attributes * `categories` - Required. List of category maps with `name` field. * `active_category` - Optional. Currently selected category name. Defaults to "All". * `sort_options` - Optional. List of sort option strings. ## Examples <.filter_bar categories={@categories} /> <.filter_bar categories={@categories} active_category="Art Prints" /> """ attr :categories, :list, required: true attr :active_category, :string, default: "All" attr :sort_options, :list, default: [ "Sort by: Featured", "Price: Low to High", "Price: High to Low", "Newest", "Best Selling" ] def filter_bar(assigns) do ~H"""
    <%= for category <- @categories do %> <% end %>
    <.shop_select options={@sort_options} class="px-4 py-2" />
    """ end @doc """ Renders a content body container for long-form content pages (about, etc.). ## Attributes * `image_url` - Optional. Header image URL. ## Slots * `inner_block` - Required. The content to render. ## Examples <.content_body image_url="/images/about.jpg">

    Content here...

    """ attr :image_url, :string, default: nil slot :inner_block, required: true def content_body(assigns) do ~H"""
    <%= if @image_url do %>
    <% end %>
    {render_slot(@inner_block)}
    """ end @doc """ Renders a contact form card. ## Attributes * `title` - Optional. Form heading. Defaults to "Send a message". * `email` - Optional. If provided, displays "Email me: [email]" below the title. ## Examples <.contact_form /> <.contact_form title="Get in touch" /> <.contact_form email="hello@example.com" /> """ attr :title, :string, default: "Send a message" attr :email, :string, default: nil attr :response_time, :string, default: nil def contact_form(assigns) do ~H""" <.shop_card class="p-8">

    {@title}

    <%= if @email || @response_time do %>
    <%= if @email do %>

    Email me: {@email}

    <% end %> <%= if @response_time do %>

    {@response_time}

    <% end %>
    <% else %>
    <% end %>
    <.shop_input type="text" placeholder="Your name" class="w-full px-4 py-2" />
    <.shop_input type="email" placeholder="your@email.com" class="w-full px-4 py-2" />
    <.shop_input type="text" placeholder="How can I help?" class="w-full px-4 py-2" />
    <.shop_textarea rows="5" placeholder="Your message..." class="w-full px-4 py-2" />
    <.shop_button type="submit" class="w-full px-6 py-3 font-semibold transition-all"> Send Message
    """ end @doc """ Renders the order tracking card. ## Examples <.order_tracking_card /> """ def order_tracking_card(assigns) do ~H""" <.shop_card class="p-6">

    Track your order

    Enter your email and I'll send you a link to check your order status.

    <.shop_input type="email" placeholder="your@email.com" class="flex-1 min-w-0 px-3 py-2 text-sm" style="min-width: 150px;" /> <.shop_button class="px-4 py-2 text-sm font-medium whitespace-nowrap"> Send
    """ end @doc """ Renders the info card with bullet points (e.g., "Handy to know" section). ## Attributes * `title` - Required. Card heading. * `items` - Required. List of maps with `label` and `value` keys. ## Examples <.info_card title="Handy to know" items={[ %{label: "Printing", value: "2-5 business days"}, %{label: "Delivery", value: "3-7 business days after printing"} ]} /> """ attr :title, :string, required: true attr :items, :list, required: true def info_card(assigns) do ~H""" <.shop_card class="p-6">

    {@title}

    """ end @doc """ Renders the contact info card with email link. ## Attributes * `title` - Optional. Card heading. Defaults to "Get in touch". * `email` - Required. Email address. * `response_text` - Optional. Response time text. Defaults to "We typically respond within 24 hours". ## Examples <.contact_info_card email="hello@example.com" /> """ attr :title, :string, default: "Get in touch" attr :email, :string, required: true attr :response_text, :string, default: "We typically respond within 24 hours" def contact_info_card(assigns) do ~H""" <.shop_card class="p-6">

    {@title}

    {@email}

    {@response_text}

    """ end @doc """ Renders a newsletter signup card. ## Attributes * `title` - Optional. Card heading. Defaults to "Stay in touch". * `description` - Optional. Card description. * `button_text` - Optional. Button text. Defaults to "Subscribe". * `variant` - Optional. Either `:card` (default, with border/background) or `:inline` (no card styling, for embedding in footer). ## Examples <.newsletter_card /> <.newsletter_card title="Studio news" description="Get updates on new products." /> <.newsletter_card variant={:inline} /> """ attr :title, :string, default: "Newsletter" attr :description, :string, default: "Get new designs and updates in your inbox. No spam, I promise." attr :button_text, :string, default: "Subscribe" attr :variant, :atom, default: :card def newsletter_card(%{variant: :inline} = assigns) do ~H"""

    {@title}

    {@description}

    <.shop_input type="email" placeholder="your@email.com" class="flex-1 min-w-0 px-4 py-2 text-sm" style="min-width: 150px;" /> <.shop_button type="submit" class="px-6 py-2 text-sm font-medium whitespace-nowrap"> {@button_text}
    """ end def newsletter_card(assigns) do ~H""" <.shop_card class="p-6">

    {@title}

    {@description}

    <.shop_input type="email" placeholder="your@email.com" class="flex-1 min-w-0 px-3 py-2 text-sm" style="min-width: 150px;" /> <.shop_button type="submit" class="px-4 py-2 text-sm font-medium whitespace-nowrap"> {@button_text}
    """ end @doc """ Renders social media links in a single card with a compact grid layout. ## Attributes * `title` - Optional. Card heading. Defaults to "Follow us". * `links` - Optional. List of maps with `platform`, `url`, and `label` keys. Supported platforms: :instagram, :pinterest, :facebook, :twitter, :tiktok, :patreon, :youtube ## Examples <.social_links_card /> <.social_links_card title="Elsewhere" links={[%{platform: :instagram, url: "https://instagram.com/example", label: "Instagram"}]} /> """ attr :title, :string, default: "Find me on" attr :links, :list, default: [ %{platform: :instagram, url: "#", label: "Instagram"}, %{platform: :pinterest, url: "#", label: "Pinterest"} ] def social_links_card(assigns) do ~H""" <.shop_card class="p-6">

    {@title}

    <%= for link <- @links do %> <.social_icon platform={link.platform} /> {link.label} <% end %>
    """ end @doc """ Renders social media icon links. ## Attributes * `links` - Optional. List of maps with `platform`, `url`, and `label` keys. Supported platforms: :instagram, :pinterest ## Examples <.social_links /> <.social_links links={[%{platform: :instagram, url: "https://instagram.com/example", label: "Instagram"}]} /> """ attr :links, :list, default: [ %{platform: :instagram, url: "#", label: "Instagram"}, %{platform: :pinterest, url: "#", label: "Pinterest"} ] def social_links(assigns) do ~H"""
    <%= for link <- @links do %> <.social_icon platform={link.platform} /> <% end %>
    """ end # Renders a social media icon for the given platform. # # All icons are from Simple Icons (simpleicons.org), MIT licensed. # # ## Supported platforms # # **Commercial/Creative:** # :instagram, :pinterest, :tiktok, :facebook, :twitter, :youtube, :patreon, :kofi, :etsy, :gumroad, :bandcamp # # **Open Web/Federated:** # :mastodon, :pixelfed, :bluesky, :peertube, :lemmy, :matrix # # **Developer/Hacker:** # :github, :gitlab, :codeberg, :sourcehut # # **Communication:** # :discord, :telegram, :signal # # **Other:** # :substack, :rss, :website attr :platform, :atom, required: true # Commercial/Creative platforms defp social_icon(%{platform: :instagram} = assigns) do ~H""" """ end defp social_icon(%{platform: :pinterest} = assigns) do ~H""" """ end defp social_icon(%{platform: :tiktok} = assigns) do ~H""" """ end defp social_icon(%{platform: :facebook} = assigns) do ~H""" """ end defp social_icon(%{platform: :twitter} = assigns) do ~H""" """ end defp social_icon(%{platform: :youtube} = assigns) do ~H""" """ end defp social_icon(%{platform: :patreon} = assigns) do ~H""" """ end defp social_icon(%{platform: :kofi} = assigns) do ~H""" """ end defp social_icon(%{platform: :etsy} = assigns) do ~H""" """ end defp social_icon(%{platform: :gumroad} = assigns) do ~H""" """ end defp social_icon(%{platform: :bandcamp} = assigns) do ~H""" """ end # Open Web/Federated platforms defp social_icon(%{platform: :mastodon} = assigns) do ~H""" """ end defp social_icon(%{platform: :pixelfed} = assigns) do ~H""" """ end defp social_icon(%{platform: :bluesky} = assigns) do ~H""" """ end defp social_icon(%{platform: :peertube} = assigns) do ~H""" """ end defp social_icon(%{platform: :lemmy} = assigns) do ~H""" """ end defp social_icon(%{platform: :matrix} = assigns) do ~H""" """ end # Developer/Hacker platforms defp social_icon(%{platform: :github} = assigns) do ~H""" """ end defp social_icon(%{platform: :gitlab} = assigns) do ~H""" """ end defp social_icon(%{platform: :codeberg} = assigns) do ~H""" """ end defp social_icon(%{platform: :sourcehut} = assigns) do ~H""" """ end # Communication platforms defp social_icon(%{platform: :discord} = assigns) do ~H""" """ end defp social_icon(%{platform: :telegram} = assigns) do ~H""" """ end defp social_icon(%{platform: :signal} = assigns) do ~H""" """ end # Other platforms defp social_icon(%{platform: :substack} = assigns) do ~H""" """ end defp social_icon(%{platform: :rss} = assigns) do ~H""" """ end defp social_icon(%{platform: :website} = assigns) do ~H""" """ end # Fallback for unknown platforms defp social_icon(assigns) do ~H""" """ end @doc """ Renders a cart item row. ## Attributes * `item` - Required. Map with `product` (containing `image_url`, `name`, `price`), `variant`, and `quantity`. * `currency` - Optional. Currency symbol. Defaults to "£". ## Examples <.cart_item item={item} /> """ attr :item, :map, required: true attr :currency, :string, default: "£" def cart_item(assigns) do ~H""" <.shop_card class="flex gap-4 p-4">
    {@item.product.name}

    {@item.product.name}

    {@item.variant}

    {@item.quantity}

    {@currency}{@item.product.price / 100 * @item.quantity}

    """ end @doc """ Renders the order summary card. ## Attributes * `subtotal` - Required. Subtotal amount (in pence/cents). * `delivery` - Optional. Delivery cost. Defaults to 800 (£8.00). * `vat` - Optional. VAT amount. Defaults to 720 (£7.20). * `currency` - Optional. Currency symbol. Defaults to "£". * `mode` - Either `:live` (default) or `:preview`. ## Examples <.order_summary subtotal={3600} /> """ attr :subtotal, :integer, required: true attr :delivery, :integer, default: 800 attr :vat, :integer, default: 720 attr :currency, :string, default: "£" attr :mode, :atom, default: :live def order_summary(assigns) do total = assigns.subtotal + assigns.delivery + assigns.vat assigns = assign(assigns, :total, total) ~H""" <.shop_card class="p-6 sticky top-4">

    Order Summary

    Subtotal {@currency}{Float.round(@subtotal / 100, 2)}
    Delivery {@currency}{Float.round(@delivery / 100, 2)}
    VAT (20%) {@currency}{Float.round(@vat / 100, 2)}
    Total {@currency}{Float.round(@total / 100, 2)}
    <.shop_button class="w-full px-6 py-3 font-semibold transition-all mb-3"> Checkout <%= if @mode == :preview do %> <.shop_button_outline phx-click="change_preview_page" phx-value-page="collection" class="w-full px-6 py-3 font-semibold transition-all" > Continue Shopping <% else %> <.shop_link_outline href="/collections/all" class="block w-full px-6 py-3 font-semibold transition-all text-center" > Continue Shopping <% end %> """ end @doc """ Renders a breadcrumb navigation. ## Attributes * `items` - Required. List of breadcrumb items. Each item is a map with: - `label` - Required. Display text - `page` - Optional. Page name for preview mode navigation - `href` - Optional. URL for live mode navigation - `current` - Optional. Boolean, if true this is the current page (not a link) * `mode` - Either `:live` (default) or `:preview`. ## Examples <.breadcrumb items={[ %{label: "Home", page: "home", href: "/"}, %{label: "Art Prints", page: "collection", href: "/collections/art-prints"}, %{label: "Mountain Sunrise", current: true} ]} mode={:preview} /> """ attr :items, :list, required: true attr :mode, :atom, default: :live def breadcrumb(assigns) do ~H""" """ end @doc """ Renders a related products section with title and product grid. ## Attributes * `title` - Optional. Section heading. Defaults to "You might also like". * `products` - Required. List of products to display. * `theme_settings` - Required. The theme settings map. * `limit` - Optional. Maximum products to show. Defaults to 4. * `mode` - Either `:live` (default) or `:preview`. ## Examples <.related_products_section products={@related_products} theme_settings={@theme_settings} mode={:preview} /> """ attr :title, :string, default: "You might also like" attr :products, :list, required: true attr :theme_settings, :map, required: true attr :limit, :integer, default: 4 attr :mode, :atom, default: :live def related_products_section(assigns) do ~H"""

    {@title}

    <.product_grid columns={:fixed_4} gap="gap-6"> <%= for product <- Enum.take(@products, @limit) do %> <.product_card product={product} theme_settings={@theme_settings} mode={@mode} variant={:compact} /> <% end %>
    """ end @doc """ Renders a star rating display. ## Attributes * `rating` - Required. Number of filled stars (1-5). * `max` - Optional. Maximum stars to display. Defaults to 5. * `size` - Optional. Size variant: `:sm` (w-4), `:md` (w-5). Defaults to `:sm`. * `color` - Optional. Star color. Defaults to "#f59e0b" (amber). ## Examples <.star_rating rating={5} /> <.star_rating rating={4} size={:md} /> """ attr :rating, :integer, required: true attr :max, :integer, default: 5 attr :size, :atom, default: :sm attr :color, :string, default: "#f59e0b" def star_rating(assigns) do size_class = if assigns.size == :md, do: "w-5 h-5", else: "w-4 h-4" assigns = assign(assigns, :size_class, size_class) ~H"""
    <%= for i <- 1..@max do %> <% end %>
    """ end @doc """ Renders trust badges (e.g., Free Delivery, Easy Returns). ## Attributes * `items` - Optional. List of badge items. Each item is a map with: - `icon` - Icon type: `:check` or `:shield` - `title` - Badge title - `description` - Badge description Defaults to Free Delivery and Easy Returns badges. ## Examples <.trust_badges /> <.trust_badges items={[%{icon: :check, title: "Custom", description: "Badge text"}]} /> """ attr :items, :list, default: [ %{icon: :check, title: "Free Delivery", description: "On orders over £40"}, %{icon: :shield, title: "Easy Returns", description: "30-day return policy"} ] def trust_badges(assigns) do ~H"""
    <%= for item <- @items do %>
    <.trust_badge_icon icon={item.icon} />

    {item.title}

    {item.description}

    <% end %>
    """ end attr :icon, :atom, required: true defp trust_badge_icon(%{icon: :check} = assigns) do ~H""" """ end defp trust_badge_icon(%{icon: :shield} = assigns) do ~H""" """ end defp trust_badge_icon(assigns) do ~H""" """ end @doc """ Renders a customer reviews section with collapsible header and review cards. ## Attributes * `reviews` - Required. List of review maps with: - `rating` - Star rating (1-5) - `title` - Review title - `body` - Review text - `author` - Reviewer name - `date` - Relative date string (e.g., "2 weeks ago") - `verified` - Boolean, if true shows "Verified purchase" badge * `average_rating` - Optional. Average rating to show in header. Defaults to 5. * `total_count` - Optional. Total number of reviews. Defaults to length of reviews list. * `open` - Optional. Whether section is expanded by default. Defaults to true. ## Examples <.reviews_section reviews={@product.reviews} average_rating={4.8} total_count={24} /> """ attr :reviews, :list, required: true attr :average_rating, :integer, default: 5 attr :total_count, :integer, default: nil attr :open, :boolean, default: true def reviews_section(assigns) do assigns = assign_new(assigns, :display_count, fn -> assigns.total_count || length(assigns.reviews) end) ~H"""

    Customer reviews

    <.star_rating rating={@average_rating} /> ({@display_count})
    <%= for review <- @reviews do %> <.review_card review={review} /> <% end %>
    <.shop_button_outline class="mt-6 px-6 py-2 text-sm font-medium transition-all mx-auto block"> Load more reviews
    """ end @doc """ Renders a single review card. ## Attributes * `review` - Required. Map with `rating`, `title`, `body`, `author`, `date`, `verified`. ## Examples <.review_card review={%{rating: 5, title: "Great!", body: "...", author: "Jane", date: "1 week ago", verified: true}} /> """ attr :review, :map, required: true def review_card(assigns) do ~H"""
    <.star_rating rating={@review.rating} /> {@review.date}

    {@review.title}

    {@review.body}

    {@review.author} <%= if @review.verified do %> Verified purchase <% end %>
    """ end @doc """ Renders a product image gallery with thumbnails and lightbox. ## Attributes * `images` - Required. List of image URLs. * `product_name` - Required. Product name for alt text. * `id_prefix` - Optional. Prefix for element IDs. Defaults to "pdp". ## Examples <.product_gallery images={@product_images} product_name={@product.name} /> """ attr :images, :list, required: true attr :product_name, :string, required: true attr :id_prefix, :string, default: "pdp" def product_gallery(assigns) do ~H"""
    {@product_name}
    <%= for {img_url, idx} <- Enum.with_index(@images) do %> <% end %>
    <.product_lightbox images={@images} product_name={@product_name} id_prefix={@id_prefix} />
    """ end # Private: Renders a product image lightbox dialog used by `product_gallery`. attr :images, :list, required: true attr :product_name, :string, required: true attr :id_prefix, :string, required: true defp product_lightbox(assigns) do ~H""" """ end @doc """ Renders product title and price information. ## Attributes * `product` - Required. Product map with `name`, `price`, `on_sale`, `compare_at_price`. * `currency` - Optional. Currency symbol. Defaults to "£". ## Examples <.product_info product={@product} /> """ attr :product, :map, required: true attr :currency, :string, default: "£" def product_info(assigns) do ~H"""

    {@product.name}

    <%= if @product.on_sale do %> {@currency}{@product.price / 100} {@currency}{@product.compare_at_price / 100} SAVE {round( (@product.compare_at_price - @product.price) / @product.compare_at_price * 100 )}% <% else %> {@currency}{@product.price / 100} <% end %>
    """ end @doc """ Renders a variant selector with button options. ## Attributes * `label` - Required. Label text (e.g., "Size", "Color"). * `options` - Required. List of option strings. * `selected` - Optional. Currently selected option. Defaults to first option. ## Examples <.variant_selector label="Size" options={["S", "M", "L", "XL"]} /> <.variant_selector label="Color" options={["Red", "Blue", "Green"]} selected="Blue" /> """ attr :label, :string, required: true attr :options, :list, required: true attr :selected, :string, default: nil def variant_selector(assigns) do assigns = assign_new(assigns, :selected_value, fn -> assigns.selected || List.first(assigns.options) end) ~H"""
    <%= for option <- @options do %> <% end %>
    """ end @doc """ Renders a quantity selector with increment/decrement buttons. ## Attributes * `quantity` - Optional. Current quantity value. Defaults to 1. * `in_stock` - Optional. Whether the product is in stock. Defaults to true. * `min` - Optional. Minimum quantity. Defaults to 1. * `max` - Optional. Maximum quantity. Defaults to 99. ## Examples <.quantity_selector /> <.quantity_selector quantity={2} in_stock={false} /> """ attr :quantity, :integer, default: 1 attr :in_stock, :boolean, default: true attr :min, :integer, default: 1 attr :max, :integer, default: 99 def quantity_selector(assigns) do ~H"""
    {@quantity}
    <%= if @in_stock do %> In stock <% else %> Out of stock <% end %>
    """ end @doc """ Renders the add to cart button. ## Attributes * `text` - Optional. Button text. Defaults to "Add to basket". * `disabled` - Optional. Whether button is disabled. Defaults to false. * `sticky` - Optional. Whether to use sticky positioning on mobile. Defaults to true. ## Examples <.add_to_cart_button /> <.add_to_cart_button text="Add to bag" disabled={true} /> """ attr :text, :string, default: "Add to basket" attr :disabled, :boolean, default: false attr :sticky, :boolean, default: true def add_to_cart_button(assigns) do ~H"""
    """ end @doc """ Renders a collapsible details/accordion item. ## Attributes * `title` - Required. Section heading text. * `open` - Optional. Whether section is expanded by default. Defaults to false. ## Slots * `inner_block` - Required. Content to show when expanded. ## Examples <.accordion_item title="Description" open={true}>

    Product description here...

    """ attr :title, :string, required: true attr :open, :boolean, default: false slot :inner_block, required: true def accordion_item(assigns) do ~H"""
    {@title}
    {render_slot(@inner_block)}
    """ end @doc """ Renders a product details accordion with Description, Size Guide, and Shipping sections. ## Attributes * `product` - Required. Product map with `description`. * `show_size_guide` - Optional. Whether to show size guide. Defaults to true. * `size_guide` - Optional. Custom size guide data. Uses default if not provided. ## Examples <.product_details product={@product} /> <.product_details product={@product} show_size_guide={false} /> """ attr :product, :map, required: true attr :show_size_guide, :boolean, default: true attr :size_guide, :list, default: nil def product_details(assigns) do assigns = assign_new(assigns, :sizes, fn -> assigns.size_guide || [ %{size: "S", chest: "86-91", length: "71"}, %{size: "M", chest: "91-96", length: "73"}, %{size: "L", chest: "96-101", length: "75"}, %{size: "XL", chest: "101-106", length: "77"} ] end) ~H"""
    <.accordion_item title="Description" open={true}>

    {@product.description}. Crafted with attention to detail and quality materials, this product is designed to last. Perfect for everyday use or special occasions.

    <%= if @show_size_guide do %> <.accordion_item title="Size Guide"> <%= for {size_row, idx} <- Enum.with_index(@sizes) do %> <% end %>
    Size Chest (cm) Length (cm)
    {size_row.size} {size_row.chest} {size_row.length}
    <% end %> <.accordion_item title="Shipping & Returns">

    Delivery

    Free UK delivery on orders over £40. Standard delivery 3-5 working days. Express delivery available at checkout.

    Returns

    We offer a 30-day return policy. Items must be unused and in original packaging. Please contact us to arrange a return.

    """ end @doc """ Renders a page title heading. ## Attributes * `text` - Required. The title text. * `class` - Optional. Additional CSS classes. ## Examples <.page_title text="Your basket" /> <.page_title text="Order History" class="mb-4" /> """ attr :text, :string, required: true attr :class, :string, default: "mb-8" def page_title(assigns) do ~H"""

    {@text}

    """ end @doc """ Renders a cart items list with order summary layout. ## Attributes * `items` - Required. List of cart items. * `subtotal` - Required. Subtotal in pence/cents. * `currency` - Optional. Currency symbol. Defaults to "£". * `mode` - Either `:live` (default) or `:preview`. ## Examples <.cart_layout items={@cart_items} subtotal={3600} mode={:preview} /> """ attr :items, :list, required: true attr :subtotal, :integer, required: true attr :currency, :string, default: "£" attr :mode, :atom, default: :live def cart_layout(assigns) do ~H"""
    <%= for item <- @items do %> <.cart_item item={item} currency={@currency} /> <% end %>
    <.order_summary subtotal={@subtotal} mode={@mode} />
    """ end @doc """ Renders rich text content with themed typography. This component renders structured content blocks (paragraphs, headings) with appropriate theme styling. ## Attributes * `blocks` - Required. List of content blocks. Each block is a map with: - `type` - Either `:paragraph`, `:heading`, or `:lead` - `text` - The text content - `level` - For headings, the level (2, 3, etc.). Defaults to 2. ## Examples <.rich_text blocks={[ %{type: :lead, text: "Introduction paragraph..."}, %{type: :paragraph, text: "Regular paragraph..."}, %{type: :heading, text: "Section Title"}, %{type: :paragraph, text: "More content..."} ]} /> """ attr :blocks, :list, required: true def rich_text(assigns) do ~H"""
    <%= for block <- @blocks do %> <.rich_text_block block={block} /> <% end %>
    """ end attr :block, :map, required: true defp rich_text_block(%{block: %{type: :lead}} = assigns) do ~H"""

    {@block.text}

    """ end defp rich_text_block(%{block: %{type: :paragraph}} = assigns) do ~H"""

    {@block.text}

    """ end defp rich_text_block(%{block: %{type: :heading}} = assigns) do ~H"""

    {@block.text}

    """ end defp rich_text_block(%{block: %{type: :closing}} = assigns) do ~H"""

    {@block.text}

    """ end defp rich_text_block(assigns) do ~H"""

    {@block.text}

    """ end @doc """ Renders a responsive `` element with AVIF, WebP, and JPEG sources. Computes available widths from `source_width` to avoid upscaling - only generates srcset entries for sizes smaller than or equal to the original image dimensions. The component renders: - `` for AVIF (best compression, modern browsers) - `` for WebP (good compression, broad support) - `` with JPEG srcset (fallback for legacy browsers) ## Attributes * `src` - Required. Base path to the image variants (without size/extension). * `alt` - Required. Alt text for accessibility. * `source_width` - Required. Original image width in pixels. * `sizes` - Optional. Responsive sizes attribute. Defaults to "100vw". * `class` - Optional. CSS classes to apply to the `` element. * `width` - Optional. Explicit width attribute. * `height` - Optional. Explicit height attribute. * `priority` - Optional. If true, sets eager loading and high fetchpriority. Defaults to false (lazy loading). ## Examples <.responsive_image src="/image_cache/abc123" source_width={1200} alt="Product image" /> <.responsive_image src="/image_cache/abc123" source_width={1200} alt="Hero banner" priority={true} sizes="(max-width: 768px) 100vw, 50vw" /> """ attr :src, :string, required: true, doc: "Base path without size/extension" attr :alt, :string, required: true attr :source_width, :integer, required: true, doc: "Original image width" attr :sizes, :string, default: "100vw" attr :class, :string, default: "" attr :width, :integer, default: nil attr :height, :integer, default: nil attr :priority, :boolean, default: false def responsive_image(assigns) do alias SimpleshopTheme.Images.Optimizer # Compute available widths from source dimensions (no upscaling) available = Optimizer.applicable_widths(assigns.source_width) default_width = Enum.max(available) assigns = assigns |> assign(:available_widths, available) |> assign(:default_width, default_width) ~H""" {@alt} """ end defp build_srcset(base, widths, format) do widths |> Enum.sort() |> Enum.map(&"#{base}-#{&1}.#{format} #{&1}w") |> Enum.join(", ") end end