feat: add mobile bottom navigation bar
Replace cramped horizontal nav on mobile with a fixed bottom tab bar for thumb-friendly navigation. The header nav is now hidden on mobile (<768px) and the bottom nav provides Home, Shop, About, and Contact links with icons. - Add mobile_bottom_nav component with icon + label nav items - Active page has accent-colored background highlight and larger icon - Add shadow to lift nav visually off the page - Update all page templates with bottom padding and bottom nav - Remove CSS rule that was overriding Tailwind's hidden class - Responsive header padding (tighter on mobile) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9c81f9511d
commit
4b22bb4a4b
@ -413,10 +413,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Shop nav display */
|
||||
.shop-nav {
|
||||
display: flex;
|
||||
}
|
||||
/* Shop nav display - hidden on mobile, flex on md+ (via Tailwind classes in component) */
|
||||
/* Note: Removed explicit display:flex here as it overrides Tailwind's hidden class */
|
||||
|
||||
/* =============================================
|
||||
STANDALONE COMPONENTS (Context-independent)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@ -24,4 +24,6 @@
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="about" mode={@mode} />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@ -16,4 +16,6 @@
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="cart" mode={@mode} />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@ -32,4 +32,6 @@
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="collection" mode={@mode} />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@ -44,4 +44,6 @@
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="contact" mode={@mode} />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@ -40,4 +40,6 @@
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="home" mode={@mode} />
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@ -41,4 +41,6 @@
|
||||
|
||||
<.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
<.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<.mobile_bottom_nav active_page="pdp" mode={@mode} />
|
||||
</div>
|
||||
|
||||
@ -53,6 +53,164 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
"""
|
||||
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"""
|
||||
<nav
|
||||
class="mobile-bottom-nav md:hidden"
|
||||
style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 100; background-color: var(--t-surface-raised); border-top: 1px solid var(--t-border-default); box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); padding-bottom: env(safe-area-inset-bottom, 0px);"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<ul class="flex justify-around items-center h-16" style="margin: 0; padding: 0; list-style: none;">
|
||||
<.mobile_nav_item
|
||||
icon={:home}
|
||||
label="Home"
|
||||
page="home"
|
||||
href="/"
|
||||
active_page={@active_page}
|
||||
mode={@mode}
|
||||
/>
|
||||
<.mobile_nav_item
|
||||
icon={:shop}
|
||||
label="Shop"
|
||||
page="collection"
|
||||
href="/collections/all"
|
||||
active_page={@active_page}
|
||||
active_pages={["collection", "pdp"]}
|
||||
mode={@mode}
|
||||
/>
|
||||
<.mobile_nav_item
|
||||
icon={:about}
|
||||
label="About"
|
||||
page="about"
|
||||
href="/about"
|
||||
active_page={@active_page}
|
||||
mode={@mode}
|
||||
/>
|
||||
<.mobile_nav_item
|
||||
icon={:contact}
|
||||
label="Contact"
|
||||
page="contact"
|
||||
href="/contact"
|
||||
active_page={@active_page}
|
||||
mode={@mode}
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
"""
|
||||
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"""
|
||||
<li class="flex-1">
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@page}
|
||||
class="flex flex-col items-center justify-center gap-1 py-2 mx-1 rounded-lg min-h-[56px]"
|
||||
style={"color: #{if @is_current, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l))", else: "var(--t-text-secondary)"}; text-decoration: none; font-weight: #{if @is_current, do: "600", else: "500"}; background-color: #{if @is_current, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1)", else: "transparent"};"}
|
||||
aria-current={if @is_current, do: "page", else: nil}
|
||||
>
|
||||
<.nav_icon icon={@icon} size={if @is_current, do: "w-6 h-6", else: "w-5 h-5"} />
|
||||
<span class="text-xs">{@label}</span>
|
||||
</a>
|
||||
<% else %>
|
||||
<a
|
||||
href={@href}
|
||||
class="flex flex-col items-center justify-center gap-1 py-2 mx-1 rounded-lg min-h-[56px]"
|
||||
style={"color: #{if @is_current, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l))", else: "var(--t-text-secondary)"}; text-decoration: none; font-weight: #{if @is_current, do: "600", else: "500"}; background-color: #{if @is_current, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1)", else: "transparent"};"}
|
||||
aria-current={if @is_current, do: "page", else: nil}
|
||||
>
|
||||
<.nav_icon icon={@icon} size={if @is_current, do: "w-6 h-6", else: "w-5 h-5"} />
|
||||
<span class="text-xs">{@label}</span>
|
||||
</a>
|
||||
<% end %>
|
||||
</li>
|
||||
"""
|
||||
end
|
||||
|
||||
defp nav_icon(%{icon: :home} = assigns) do
|
||||
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
|
||||
|
||||
~H"""
|
||||
<svg class={@size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path>
|
||||
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
defp nav_icon(%{icon: :shop} = assigns) do
|
||||
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
|
||||
|
||||
~H"""
|
||||
<svg class={@size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<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>
|
||||
"""
|
||||
end
|
||||
|
||||
defp nav_icon(%{icon: :about} = assigns) do
|
||||
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
|
||||
|
||||
~H"""
|
||||
<svg class={@size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
defp nav_icon(%{icon: :contact} = assigns) do
|
||||
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
|
||||
|
||||
~H"""
|
||||
<svg class={@size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
|
||||
<polyline points="22,6 12,13 2,6"></polyline>
|
||||
</svg>
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the search modal overlay.
|
||||
|
||||
@ -250,8 +408,8 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
def shop_header(assigns) do
|
||||
~H"""
|
||||
<header
|
||||
class="shop-header"
|
||||
style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); padding: 1rem 2rem; display: flex; align-items: center;"
|
||||
class="shop-header px-2 py-2 sm:px-4 sm:py-3 md:px-8 md:py-4"
|
||||
style="background-color: var(--t-surface-raised); border-bottom: 1px solid var(--t-border-default); display: flex; align-items: center;"
|
||||
>
|
||||
<%= if @theme_settings.header_background_enabled && @header_image do %>
|
||||
<div style={header_background_style(@theme_settings, @header_image)} />
|
||||
@ -266,7 +424,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav class="shop-nav hidden md:flex" style="gap: 1.5rem; position: relative; z-index: 1;">
|
||||
<nav class="shop-nav hidden md:flex md:gap-6" style="position: relative; z-index: 1;">
|
||||
<%= if @mode == :preview do %>
|
||||
<.nav_item label="Home" page="home" active_page={@active_page} mode={:preview} />
|
||||
<.nav_item label="Shop" page="collection" active_page={@active_page} mode={:preview} active_pages={["collection", "pdp"]} />
|
||||
@ -280,7 +438,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
<% end %>
|
||||
</nav>
|
||||
|
||||
<div class="shop-actions flex items-center gap-1" style="position: relative; z-index: 1;">
|
||||
<div class="shop-actions flex items-center" style="position: relative; z-index: 1;">
|
||||
<button
|
||||
type="button"
|
||||
class="header-icon-btn w-9 h-9 flex items-center justify-center transition-all"
|
||||
@ -1456,7 +1614,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
## Attributes
|
||||
|
||||
* `title` - Optional. Form heading. Defaults to "Send a message".
|
||||
* `email` - Optional. If provided, displays "or email [email]" below the title.
|
||||
* `email` - Optional. If provided, displays "Email me: [email]" below the title.
|
||||
|
||||
## Examples
|
||||
|
||||
@ -1480,7 +1638,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
<%= if @email || @response_time do %>
|
||||
<div class="mb-6 text-sm space-y-1" style="color: var(--t-text-secondary);">
|
||||
<%= if @email do %>
|
||||
<p>or email <a href={"mailto:#{@email}"} style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"><%= @email %></a></p>
|
||||
<p>Email me: <a href={"mailto:#{@email}"} style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"><%= @email %></a></p>
|
||||
<% end %>
|
||||
<%= if @response_time do %>
|
||||
<p><%= @response_time %></p>
|
||||
@ -1677,7 +1835,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
<.newsletter_card variant={:inline} />
|
||||
"""
|
||||
attr :title, :string, default: "Newsletter"
|
||||
attr :description, :string, default: "New designs and updates. No spam."
|
||||
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
|
||||
|
||||
@ -1752,7 +1910,7 @@ defmodule SimpleshopThemeWeb.ShopComponents do
|
||||
<.social_links_card />
|
||||
<.social_links_card title="Elsewhere" links={[%{platform: :instagram, url: "https://instagram.com/example", label: "Instagram"}]} />
|
||||
"""
|
||||
attr :title, :string, default: "Find me online"
|
||||
attr :title, :string, default: "Find me on"
|
||||
attr :links, :list, default: [
|
||||
%{platform: :instagram, url: "#", label: "Instagram"},
|
||||
%{platform: :pinterest, url: "#", label: "Pinterest"}
|
||||
|
||||
@ -100,7 +100,7 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="shop-container min-h-screen" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<div class="shop-container min-h-screen pb-20 md:pb-0" style="background-color: var(--t-surface-base); font-family: var(--t-font-body); color: var(--t-text-primary);">
|
||||
<SimpleshopThemeWeb.ShopComponents.skip_link />
|
||||
|
||||
<%= if @theme_settings.announcement_bar do %>
|
||||
@ -158,6 +158,8 @@ defmodule SimpleshopThemeWeb.ShopLive.Collection do
|
||||
<SimpleshopThemeWeb.ShopComponents.cart_drawer cart_items={@cart_items} subtotal={@cart_subtotal} mode={@mode} />
|
||||
|
||||
<SimpleshopThemeWeb.ShopComponents.search_modal hint_text={~s(Try searching for "mountain", "forest", or "ocean")} />
|
||||
|
||||
<SimpleshopThemeWeb.ShopComponents.mobile_bottom_nav active_page="collection" mode={@mode} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user