feat: enhance gallery page header with responsive social media links and view mode options

- Updated `PageHeaderLeft` to conditionally display social media links on larger screens.
- Introduced `MoreActionMenu` in `PageHeaderRight` for mobile users, allowing access to view mode settings and social links.
- Modified `ViewModeSegment` to ensure consistent display across different screen sizes.
- Enhanced `DropdownMenuItem` to support child components for better flexibility in dropdown menus.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-30 23:12:36 +08:00
parent b6b7941ea2
commit 5763ec8dba
4 changed files with 136 additions and 32 deletions

View File

@@ -43,7 +43,7 @@ export const PageHeaderLeft = () => {
<span className="text-xs text-white/40 lg:text-sm">{visiblePhotoCount}</span>
</div>
{(githubUrl || twitterUrl || hasRss) && (
<div className="ml-1 flex items-center gap-1 border-l border-white/10 pl-2">
<div className="ml-1 hidden items-center gap-1 border-l border-white/10 pl-2 lg:flex">
{githubUrl && <SocialIconButton icon="i-mingcute-github-fill" title="GitHub" href={githubUrl} />}
{twitterUrl && <SocialIconButton icon="i-mingcute-twitter-fill" title="Twitter" href={twitterUrl} />}
{hasRss && <SocialIconButton icon="i-mingcute-rss-2-fill" title="RSS" href="/feed.xml" />}

View File

@@ -14,13 +14,13 @@ import { Drawer } from 'vaul'
import { gallerySettingAtom, isCommandPaletteOpenAtom } from '~/atoms/app'
import { sessionUserAtom } from '~/atoms/session'
import { injectConfig } from '~/config'
import { injectConfig, siteConfig } from '~/config'
import { useMobile } from '~/hooks/useMobile'
import { authApi } from '~/lib/api/auth'
import { UserAvatar } from '../../social/comments/UserAvatar'
import { ViewPanel } from '../panels/ViewPanel'
import { ActionIconButton } from './utils'
import { ActionIconButton, resolveSocialUrl } from './utils'
export const PageHeaderRight = () => {
const { t } = useTranslation()
@@ -77,6 +77,8 @@ export const PageHeaderRight = () => {
<ViewPanel />
</DesktopViewButton>
)}
{isMobile && <MoreActionMenu />}
</div>
{/* Auth Section - Only show when useCloud is true */}
@@ -89,6 +91,84 @@ export const PageHeaderRight = () => {
)
}
const MoreActionMenu = () => {
const { t } = useTranslation()
const [settings, setSettings] = useAtom(gallerySettingAtom)
const githubUrl =
siteConfig.social && siteConfig.social.github
? resolveSocialUrl(siteConfig.social.github, { baseUrl: 'https://github.com/' })
: undefined
const twitterUrl =
siteConfig.social && siteConfig.social.twitter
? resolveSocialUrl(siteConfig.social.twitter, { baseUrl: 'https://twitter.com/', stripAt: true })
: undefined
const hasRss = true
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="relative flex size-7 items-center justify-center rounded text-white/60 transition-all duration-200 hover:bg-white/10 hover:text-white lg:hidden"
>
<i className="i-mingcute-more-2-line text-lg" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[180px]">
<div className="px-2 py-1.5 text-xs font-medium text-white/50">{t('action.view.title')}</div>
<DropdownMenuItem
onClick={() => setSettings((prev) => ({ ...prev, viewMode: 'masonry' }))}
className="justify-between"
>
<span className="flex items-center gap-2">
<i className="i-mingcute-grid-line text-base" />
{t('gallery.view.masonry')}
</span>
{settings.viewMode === 'masonry' && <i className="i-mingcute-check-line text-base" />}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setSettings((prev) => ({ ...prev, viewMode: 'list' }))}
className="justify-between"
>
<span className="flex items-center gap-2">
<i className="i-mingcute-list-ordered-line text-base" />
{t('gallery.view.list')}
</span>
{settings.viewMode === 'list' && <i className="i-mingcute-check-line text-base" />}
</DropdownMenuItem>
{(githubUrl || twitterUrl || hasRss) && <DropdownMenuSeparator />}
{githubUrl && (
<DropdownMenuItem asChild>
<a href={githubUrl} target="_blank" rel="noreferrer" className="flex items-center gap-2">
<i className="i-mingcute-github-fill text-base" />
GitHub
</a>
</DropdownMenuItem>
)}
{twitterUrl && (
<DropdownMenuItem asChild>
<a href={twitterUrl} target="_blank" rel="noreferrer" className="flex items-center gap-2">
<i className="i-mingcute-twitter-fill text-base" />
Twitter
</a>
</DropdownMenuItem>
)}
{hasRss && (
<DropdownMenuItem asChild>
<a href="/feed.xml" target="_blank" rel="noreferrer" className="flex items-center gap-2">
<i className="i-mingcute-rss-2-fill text-base" />
RSS
</a>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)
}
// 紧凑版本的桌面端视图按钮
const DesktopViewButton = ({
icon,

View File

@@ -15,7 +15,7 @@ export const ViewModeSegment = () => {
}
return (
<div className="bg-material-medium/40 relative flex h-7 items-center gap-0.5 rounded-lg p-0.5 lg:h-8 lg:gap-1 lg:p-1">
<div className="bg-material-medium/40 relative hidden h-7 items-center gap-0.5 rounded-lg p-0.5 lg:flex lg:h-8 lg:gap-1 lg:p-1">
<button
type="button"
onClick={() => handleViewModeChange('masonry')}

View File

@@ -78,6 +78,8 @@ const DropdownMenuItem = ({
active,
highlightColor: _highlightColor = 'accent',
shortcut: _shortcut,
asChild,
children,
...props
}: React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
@@ -87,35 +89,57 @@ const DropdownMenuItem = ({
shortcut?: string
} & {
ref?: React.Ref<React.ElementRef<typeof DropdownMenuPrimitive.Item> | null>
}) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={clsxm(
'cursor-menu relative flex select-none items-center rounded-lg px-2.5 py-1 outline-none data-disabled:pointer-events-none data-disabled:opacity-50',
'focus-within:outline-transparent text-sm my-0.5 transition-all duration-200',
'data-highlighted:text-accent',
'h-[28px]',
inset && 'pl-8',
className,
)}
style={{
// @ts-ignore - CSS variable for data-highlighted state
'--highlight-bg':
'linear-gradient(to right, color-mix(in srgb, var(--color-accent) 8%, transparent), color-mix(in srgb, var(--color-accent) 5%, transparent))',
}}
{...props}
>
{!!icon && (
<span className="mr-1.5 inline-flex size-4 items-center justify-center">
{typeof icon === 'function' ? icon({ isActive: active }) : icon}
</span>
)}
{props.children}
}) => {
const mergedClassName = clsxm(
'cursor-menu relative flex select-none items-center rounded-lg px-2.5 py-1 outline-none data-disabled:pointer-events-none data-disabled:opacity-50',
'focus-within:outline-transparent text-sm my-0.5 transition-all duration-200',
'data-highlighted:text-accent',
'h-[28px]',
inset && 'pl-8',
className,
)
{/* Justify Fill */}
{!!icon && <span className="ml-1.5 size-4" />}
</DropdownMenuPrimitive.Item>
)
if (asChild) {
return (
<DropdownMenuPrimitive.Item
ref={ref}
className={mergedClassName}
style={{
// @ts-ignore - CSS variable for data-highlighted state
'--highlight-bg':
'linear-gradient(to right, color-mix(in srgb, var(--color-accent) 8%, transparent), color-mix(in srgb, var(--color-accent) 5%, transparent))',
}}
asChild
{...props}
>
{children}
</DropdownMenuPrimitive.Item>
)
}
return (
<DropdownMenuPrimitive.Item
ref={ref}
className={mergedClassName}
style={{
// @ts-ignore - CSS variable for data-highlighted state
'--highlight-bg':
'linear-gradient(to right, color-mix(in srgb, var(--color-accent) 8%, transparent), color-mix(in srgb, var(--color-accent) 5%, transparent))',
}}
{...props}
>
{!!icon && (
<span className="mr-1.5 inline-flex size-4 items-center justify-center">
{typeof icon === 'function' ? icon({ isActive: active }) : icon}
</span>
)}
{children}
{/* Justify Fill */}
{!!icon && <span className="ml-1.5 size-4" />}
</DropdownMenuPrimitive.Item>
)
}
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = ({