Merge pull request #401 from Luca-Timo/feat/gallery-header-cleanup

feat(gallery): icon-only menu, accent Download CTA (#386)
This commit is contained in:
Paul Nothaft
2026-05-07 11:33:34 +02:00
committed by GitHub
2 changed files with 107 additions and 23 deletions
@@ -40,6 +40,16 @@ interface GalleryLayoutProps {
showDownloadAll?: boolean;
onDownloadAll?: () => void;
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;
menuButton?: React.ReactNode;
headerStyle?: HeaderStyleType;
@@ -54,6 +64,8 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
showDownloadAll = false,
onDownloadAll,
isDownloading = false,
showHeaderDownload = false,
onHeaderDownload,
headerExtra,
menuButton,
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' : ''}`}>
{/* Standard / Banner header - full bar with logo, event info, and actions (all layouts) */}
{!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">
{/* Left side - Menu button, Logo */}
<div className="flex items-center gap-2 sm:gap-4 flex-shrink-0">
{/* Menu button */}
{menuButton && (
<div className="flex-shrink-0">
{menuButton}
</div>
)}
{/* Left side - Logo (menu lives in the absolute wrapper above) */}
<div className={`flex items-center gap-2 sm:gap-4 flex-shrink-0 ${menuButton ? 'pl-12 sm:pl-14' : ''}`}>
{/* Logo - Show custom logo or fallback to PicPeak logo */}
{shouldShowLogo('header') && (
<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>
)}
{/*
* "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 */}
{showLogout && onLogout && (
<Button
@@ -301,6 +339,23 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
<span className="hidden sm:inline">{t('gallery.downloadAll')}</span>
</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 && (
<Button
variant="outline"
@@ -381,6 +436,24 @@ export const GalleryLayout: React.FC<GalleryLayoutProps> = ({
</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 */}
{showLogout && onLogout && (
<Button
+24 -13
View File
@@ -731,21 +731,32 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
headerStyle={data?.event?.header_style || theme.headerStyle}
showLogout={true}
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}
isDownloading={downloadAllMutation.isPending}
menuButton={showSidebar ? (
<Button
variant="ghost"
size="sm"
className="gallery-btn"
leftIcon={<Menu className="w-4 h-4" />}
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={t('gallery.toggleMenu')}
>
<span className="hidden sm:inline">{t('common.menu')}</span>
</Button>
) : undefined}
menuButton={
// Menu icon is shown when the event theme uses the sidebar
// controls style. The button is icon-only — the redundant
// "Menu" text label was dropped (#386). The wrapper aligns the
// icon to the very left of the header so the logo lines up
// with the leftmost gallery image.
showSidebar ? (
<Button
variant="ghost"
size="sm"
className="gallery-btn p-2"
onClick={() => setSidebarOpen(!sidebarOpen)}
aria-label={t('gallery.toggleMenu')}
>
<Menu className="w-5 h-5" />
</Button>
) : undefined
}
showHeaderDownload={allowDownloads}
onHeaderDownload={handleDownloadAll}
headerExtra={(() => {
const items = [];