feat(gallery): icon-only menu, accent Download CTA, logo aligned (#386)
Addresses the-luap/picpeak#386 — gallery header layout cleanup. - Drop the redundant "Menu" text label; menu button is icon-only with tight padding (p-2). - Absolute-position the menu icon at the very left of the header so it no longer pushes the logo right with every other action. Logo wrapper picks up pl-12 sm:pl-14 only when a menu button is rendered, so the icon and logo don't overlap. When no menu button (controlsStyle: classic), logo is flush with .container. - New accent-coloured "Download" CTA placed immediately left of Logout. Always visible when downloads are allowed; replaces the previous primary-coloured "Download All" header button. Same CTA appears in standard, hero, and minimal headers. Intentionally NOT shown in the no-header variant (chromeless by design). - Coloured via var(--color-accent) inline so the button automatically tracks whatever palette the admin has chosen — works on plain beta today (#22c55e) and auto-upgrades to the CI accent when #400 lands. The sidebar's own Download All is untouched. Old showDownloadAll prop stays on GalleryLayout for back-compat; GalleryView now passes showDownloadAll={false} so only the new accent button renders in the header. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
@@ -40,6 +40,16 @@ interface GalleryLayoutProps {
|
|||||||
showDownloadAll?: boolean;
|
showDownloadAll?: boolean;
|
||||||
onDownloadAll?: () => void;
|
onDownloadAll?: () => void;
|
||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
|
/**
|
||||||
|
* "Download" CTA shown immediately to the left of the Logout button in the
|
||||||
|
* standard / banner header. Same handler as Download All; the label and
|
||||||
|
* placement are intentionally simpler — single primary action right before
|
||||||
|
* Logout, the natural step at the end of a gallery visit (#386). Always
|
||||||
|
* visible when allowed (independent of sidebar state) so guests aren't
|
||||||
|
* forced to discover the download in the menu.
|
||||||
|
*/
|
||||||
|
showHeaderDownload?: boolean;
|
||||||
|
onHeaderDownload?: () => void;
|
||||||
headerExtra?: React.ReactNode;
|
headerExtra?: React.ReactNode;
|
||||||
menuButton?: React.ReactNode;
|
menuButton?: React.ReactNode;
|
||||||
headerStyle?: HeaderStyleType;
|
headerStyle?: HeaderStyleType;
|
||||||
@@ -54,6 +64,8 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
showDownloadAll = false,
|
showDownloadAll = false,
|
||||||
onDownloadAll,
|
onDownloadAll,
|
||||||
isDownloading = false,
|
isDownloading = false,
|
||||||
|
showHeaderDownload = false,
|
||||||
|
onHeaderDownload,
|
||||||
headerExtra,
|
headerExtra,
|
||||||
menuButton,
|
menuButton,
|
||||||
headerStyle: headerStyleProp,
|
headerStyle: headerStyleProp,
|
||||||
@@ -154,17 +166,22 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
<header className={`gallery-header bg-surface border-b border-surface sticky top-0 z-40 ${isHeroHeader || isBannerHeader ? 'shadow-sm' : ''}`}>
|
<header className={`gallery-header bg-surface border-b border-surface sticky top-0 z-40 ${isHeroHeader || isBannerHeader ? 'shadow-sm' : ''}`}>
|
||||||
{/* Standard / Banner header - full bar with logo, event info, and actions (all layouts) */}
|
{/* Standard / Banner header - full bar with logo, event info, and actions (all layouts) */}
|
||||||
{!isHeroHeader && !isMinimalHeader && !isNoHeader && (
|
{!isHeroHeader && !isMinimalHeader && !isNoHeader && (
|
||||||
<div className="container py-3">
|
<div className="container py-3 relative">
|
||||||
|
{/*
|
||||||
|
* Menu icon is absolute-positioned at the very left of the header
|
||||||
|
* row instead of sitting inside the flex flow, so the logo's left
|
||||||
|
* edge can align with the leftmost gallery image (both anchored at
|
||||||
|
* `.container` left padding) — see #386. The icon stays vertically
|
||||||
|
* centred via top-1/2 + -translate-y-1/2.
|
||||||
|
*/}
|
||||||
|
{menuButton && (
|
||||||
|
<div className="absolute left-3 sm:left-6 lg:left-8 top-1/2 -translate-y-1/2 z-10">
|
||||||
|
{menuButton}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between gap-2 sm:gap-4">
|
<div className="flex items-center justify-between gap-2 sm:gap-4">
|
||||||
{/* Left side - Menu button, Logo */}
|
{/* Left side - Logo (menu lives in the absolute wrapper above) */}
|
||||||
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
|
<div className={`flex items-center gap-2 sm:gap-4 flex-shrink-0 ${menuButton ? 'pl-12 sm:pl-14' : ''}`}>
|
||||||
{/* Menu button */}
|
|
||||||
{menuButton && (
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
{menuButton}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Logo - Show custom logo or fallback to PicPeak logo */}
|
{/* Logo - Show custom logo or fallback to PicPeak logo */}
|
||||||
{shouldShowLogo('header') && (
|
{shouldShowLogo('header') && (
|
||||||
<div className={`gallery-logo-wrapper flex-shrink-0 flex items-center gap-2 ${brandingSettings?.logo_position === 'center' ? 'flex-1' : ''} ${getLogoPositionClass()}`}>
|
<div className={`gallery-logo-wrapper flex-shrink-0 flex items-center gap-2 ${brandingSettings?.logo_position === 'center' ? 'flex-1' : ''} ${getLogoPositionClass()}`}>
|
||||||
@@ -239,6 +256,27 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/*
|
||||||
|
* "Download" CTA — accent-coloured button immediately left of
|
||||||
|
* Logout, always visible when the gallery allows downloads.
|
||||||
|
* Uses var(--color-accent) inline so the same colour token
|
||||||
|
* shared with the 8-token palette (PR #400) resolves to the
|
||||||
|
* admin's chosen accent regardless of which PR merges first.
|
||||||
|
*/}
|
||||||
|
{showHeaderDownload && onHeaderDownload && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onHeaderDownload}
|
||||||
|
disabled={isDownloading}
|
||||||
|
aria-label={t('gallery.download', 'Download')}
|
||||||
|
className="gallery-btn gallery-btn-download inline-flex items-center gap-2 px-3 sm:px-4 h-9 rounded-lg text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
style={{ backgroundColor: 'var(--color-accent)', color: '#ffffff' }}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">{t('gallery.download', 'Download')}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Logout button */}
|
{/* Logout button */}
|
||||||
{showLogout && onLogout && (
|
{showLogout && onLogout && (
|
||||||
<Button
|
<Button
|
||||||
@@ -301,6 +339,23 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
|
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{/* Accent Download CTA — also rendered in the minimal header
|
||||||
|
so the action stays one click away regardless of header
|
||||||
|
style. Intentionally NOT shown in the no-header variant
|
||||||
|
where the gallery is fully chromeless by design. */}
|
||||||
|
{showHeaderDownload && onHeaderDownload && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onHeaderDownload}
|
||||||
|
disabled={isDownloading}
|
||||||
|
aria-label={t('gallery.download', 'Download')}
|
||||||
|
className="gallery-btn gallery-btn-download inline-flex items-center gap-2 px-3 sm:px-4 h-9 rounded-lg text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
style={{ backgroundColor: 'var(--color-accent)', color: '#ffffff' }}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">{t('gallery.download', 'Download')}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{showLogout && onLogout && (
|
{showLogout && onLogout && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -381,6 +436,24 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Accent Download CTA — also rendered above the hero so the
|
||||||
|
primary download action is reachable without scrolling.
|
||||||
|
Intentionally NOT shown in the no-header variant where
|
||||||
|
the gallery is fully chromeless by design. */}
|
||||||
|
{showHeaderDownload && onHeaderDownload && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onHeaderDownload}
|
||||||
|
disabled={isDownloading}
|
||||||
|
aria-label={t('gallery.download', 'Download')}
|
||||||
|
className="gallery-btn gallery-btn-download inline-flex items-center gap-2 px-3 sm:px-4 h-9 rounded-lg text-sm font-medium transition-opacity hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
style={{ backgroundColor: 'var(--color-accent)', color: '#ffffff' }}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">{t('gallery.download', 'Download')}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Logout button */}
|
{/* Logout button */}
|
||||||
{showLogout && onLogout && (
|
{showLogout && onLogout && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -729,21 +729,32 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
|
|||||||
headerStyle={data?.event?.header_style || theme.headerStyle}
|
headerStyle={data?.event?.header_style || theme.headerStyle}
|
||||||
showLogout={true}
|
showLogout={true}
|
||||||
onLogout={logout}
|
onLogout={logout}
|
||||||
showDownloadAll={!showSidebar && allowDownloads}
|
// Old Download All header button is replaced by the new
|
||||||
|
// showHeaderDownload below — accent-coloured, always visible when
|
||||||
|
// downloads are allowed, sits right before Logout (#386).
|
||||||
|
showDownloadAll={false}
|
||||||
onDownloadAll={handleDownloadAll}
|
onDownloadAll={handleDownloadAll}
|
||||||
isDownloading={downloadAllMutation.isPending}
|
isDownloading={downloadAllMutation.isPending}
|
||||||
menuButton={showSidebar ? (
|
menuButton={
|
||||||
<Button
|
// Menu icon is shown when the event theme uses the sidebar
|
||||||
variant="ghost"
|
// controls style. The button is icon-only — the redundant
|
||||||
size="sm"
|
// "Menu" text label was dropped (#386). The wrapper aligns the
|
||||||
className="gallery-btn"
|
// icon to the very left of the header so the logo lines up
|
||||||
leftIcon={<Menu className="w-4 h-4" />}
|
// with the leftmost gallery image.
|
||||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
showSidebar ? (
|
||||||
aria-label={t('gallery.toggleMenu')}
|
<Button
|
||||||
>
|
variant="ghost"
|
||||||
<span className="hidden sm:inline">{t('common.menu')}</span>
|
size="sm"
|
||||||
</Button>
|
className="gallery-btn p-2"
|
||||||
) : undefined}
|
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
|
aria-label={t('gallery.toggleMenu')}
|
||||||
|
>
|
||||||
|
<Menu className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
showHeaderDownload={allowDownloads}
|
||||||
|
onHeaderDownload={handleDownloadAll}
|
||||||
headerExtra={(() => {
|
headerExtra={(() => {
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user