feat: optimize small screen titlebar styles
This commit is contained in:
parent
8e0b91888f
commit
d756d8fc2f
22 changed files with 152 additions and 68 deletions
|
|
@ -5,12 +5,12 @@ import { LogIn } from 'lucide-react'
|
||||||
export default function LoginButton({
|
export default function LoginButton({
|
||||||
variant = 'titlebar'
|
variant = 'titlebar'
|
||||||
}: {
|
}: {
|
||||||
variant?: 'titlebar' | 'sidebar'
|
variant?: 'titlebar' | 'sidebar' | 'small-screen-titlebar'
|
||||||
}) {
|
}) {
|
||||||
const { checkLogin } = useNostr()
|
const { checkLogin } = useNostr()
|
||||||
|
|
||||||
let triggerComponent: React.ReactNode
|
let triggerComponent: React.ReactNode
|
||||||
if (variant === 'titlebar') {
|
if (variant === 'titlebar' || variant === 'small-screen-titlebar') {
|
||||||
triggerComponent = <LogIn />
|
triggerComponent = <LogIn />
|
||||||
} else {
|
} else {
|
||||||
triggerComponent = (
|
triggerComponent = (
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export default function ProfileButton({
|
||||||
variant = 'titlebar'
|
variant = 'titlebar'
|
||||||
}: {
|
}: {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
variant?: 'titlebar' | 'sidebar'
|
variant?: 'titlebar' | 'sidebar' | 'small-screen-titlebar'
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { logout } = useNostr()
|
const { logout } = useNostr()
|
||||||
|
|
@ -33,7 +33,18 @@ export default function ProfileButton({
|
||||||
if (variant === 'titlebar') {
|
if (variant === 'titlebar') {
|
||||||
triggerComponent = (
|
triggerComponent = (
|
||||||
<button>
|
<button>
|
||||||
<Avatar className="w-6 h-6 hover:opacity-90">
|
<Avatar className="ml-2 w-7 h-7 hover:opacity-90">
|
||||||
|
<AvatarImage src={avatar} />
|
||||||
|
<AvatarFallback>
|
||||||
|
<img src={defaultAvatar} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
} else if (variant === 'small-screen-titlebar') {
|
||||||
|
triggerComponent = (
|
||||||
|
<button>
|
||||||
|
<Avatar className="w-8 h-8 hover:opacity-90">
|
||||||
<AvatarImage src={avatar} />
|
<AvatarImage src={avatar} />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
<img src={defaultAvatar} />
|
<img src={defaultAvatar} />
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import ProfileButton from './ProfileButton'
|
||||||
export default function AccountButton({
|
export default function AccountButton({
|
||||||
variant = 'titlebar'
|
variant = 'titlebar'
|
||||||
}: {
|
}: {
|
||||||
variant?: 'titlebar' | 'sidebar'
|
variant?: 'titlebar' | 'sidebar' | 'small-screen-titlebar'
|
||||||
}) {
|
}) {
|
||||||
const { pubkey } = useNostr()
|
const { pubkey } = useNostr()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,20 @@ import { useSecondaryPage } from '@renderer/PageManager'
|
||||||
import { ChevronLeft } from 'lucide-react'
|
import { ChevronLeft } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function BackButton({ hide = false }: { hide?: boolean }) {
|
export default function BackButton({
|
||||||
|
hide = false,
|
||||||
|
variant = 'titlebar'
|
||||||
|
}: {
|
||||||
|
hide?: boolean
|
||||||
|
variant?: 'titlebar' | 'small-screen-titlebar'
|
||||||
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pop } = useSecondaryPage()
|
const { pop } = useSecondaryPage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!hide && (
|
{!hide && (
|
||||||
<Button variant="titlebar" size="titlebar" title={t('back')} onClick={() => pop()}>
|
<Button variant={variant} size={variant} title={t('back')} onClick={() => pop()}>
|
||||||
<ChevronLeft />
|
<ChevronLeft />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,13 @@ const Content = memo(
|
||||||
if (embeddedNotes.length) {
|
if (embeddedNotes.length) {
|
||||||
embeddedNotes.forEach((note, index) => {
|
embeddedNotes.forEach((note, index) => {
|
||||||
const id = note.split(':')[1]
|
const id = note.split(':')[1]
|
||||||
nodes.push(<EmbeddedNote key={`embedded-event-${index}`} noteId={id} />)
|
nodes.push(
|
||||||
|
<EmbeddedNote
|
||||||
|
key={`embedded-event-${index}`}
|
||||||
|
noteId={id}
|
||||||
|
className={size === 'small' ? 'mt-1' : 'mt-2'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { useFetchEvent } from '@renderer/hooks'
|
import { useFetchEvent } from '@renderer/hooks'
|
||||||
import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@renderer/lib/link'
|
import { toNoStrudelArticle, toNoStrudelNote, toNoStrudelStream } from '@renderer/lib/link'
|
||||||
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { kinds } from 'nostr-tools'
|
import { kinds } from 'nostr-tools'
|
||||||
import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard'
|
import ShortTextNoteCard from '../NoteCard/ShortTextNoteCard'
|
||||||
|
|
||||||
export function EmbeddedNote({ noteId }: { noteId: string }) {
|
export function EmbeddedNote({ noteId, className }: { noteId: string; className?: string }) {
|
||||||
const { event } = useFetchEvent(noteId)
|
const { event } = useFetchEvent(noteId)
|
||||||
|
|
||||||
return event && event.kind === kinds.ShortTextNote ? (
|
return event && event.kind === kinds.ShortTextNote ? (
|
||||||
<ShortTextNoteCard className="mt-2 w-full" event={event} embedded />
|
<ShortTextNoteCard className={cn('w-full', className)} event={event} embedded />
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href={
|
href={
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export default function Note({
|
||||||
)}
|
)}
|
||||||
<Content className="mt-2" event={event} />
|
<Content className="mt-2" event={event} />
|
||||||
{!hideStats && (
|
{!hideStats && (
|
||||||
<NoteStats className="mt-4" event={event} fetchIfNotExisting={fetchNoteStats} />
|
<NoteStats className="mt-3 sm:mt-4" event={event} fetchIfNotExisting={fetchNoteStats} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default function ShortTextNoteCard({
|
||||||
>
|
>
|
||||||
<RepostDescription reposter={reposter} className="max-sm:hidden pl-4" />
|
<RepostDescription reposter={reposter} className="max-sm:hidden pl-4" />
|
||||||
<div
|
<div
|
||||||
className={`hover:bg-muted/50 text-left cursor-pointer ${embedded ? 'p-3 border rounded-lg' : 'p-4 sm:border sm:rounded-lg max-sm:border-b'}`}
|
className={`hover:bg-muted/50 text-left cursor-pointer ${embedded ? 'p-2 sm:p-3 border rounded-lg' : 'px-4 py-3 sm:py-4 sm:border sm:rounded-lg max-sm:border-b'}`}
|
||||||
>
|
>
|
||||||
<RepostDescription reposter={reposter} className="sm:hidden" />
|
<RepostDescription reposter={reposter} className="sm:hidden" />
|
||||||
<Note
|
<Note
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ import { PencilLine } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function PostButton({ variant = 'titlebar' }: { variant?: 'titlebar' | 'sidebar' }) {
|
export default function PostButton({
|
||||||
|
variant = 'titlebar'
|
||||||
|
}: {
|
||||||
|
variant?: 'titlebar' | 'sidebar' | 'small-screen-titlebar'
|
||||||
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'
|
||||||
export default function RelaySettingsButton({
|
export default function RelaySettingsButton({
|
||||||
variant = 'titlebar'
|
variant = 'titlebar'
|
||||||
}: {
|
}: {
|
||||||
variant?: 'titlebar' | 'sidebar'
|
variant?: 'titlebar' | 'sidebar' | 'small-screen-titlebar'
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ export default function ReplyNoteList({ event, className }: { event: Event; clas
|
||||||
>
|
>
|
||||||
{loading ? t('loading...') : until ? t('load more older replies') : null}
|
{loading ? t('loading...') : until ? t('load more older replies') : null}
|
||||||
</div>
|
</div>
|
||||||
{replies.length > 0 && (loading || until) && <Separator className="my-4" />}
|
{replies.length > 0 && (loading || until) && <Separator className="my-2" />}
|
||||||
<div className={cn('mb-4', className)}>
|
<div className={cn('mb-4', className)}>
|
||||||
{replies.map((reply, index) => {
|
{replies.map((reply, index) => {
|
||||||
const info = replyMap[reply.id]
|
const info = replyMap[reply.id]
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,25 @@
|
||||||
import { Button } from '@renderer/components/ui/button'
|
import { Button } from '@renderer/components/ui/button'
|
||||||
import { cn } from '@renderer/lib/utils'
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { ChevronUp } from 'lucide-react'
|
import { ChevronUp } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
export default function ScrollToTopButton({
|
export default function ScrollToTopButton({
|
||||||
scrollAreaRef,
|
scrollAreaRef,
|
||||||
className
|
className,
|
||||||
|
visible = true
|
||||||
}: {
|
}: {
|
||||||
scrollAreaRef: React.RefObject<HTMLDivElement>
|
scrollAreaRef: React.RefObject<HTMLDivElement>
|
||||||
className?: string
|
className?: string
|
||||||
|
visible?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [showScrollToTop, setShowScrollToTop] = useState(false)
|
|
||||||
|
|
||||||
const handleScrollToTop = () => {
|
const handleScrollToTop = () => {
|
||||||
scrollAreaRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
scrollAreaRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleScroll = () => {
|
|
||||||
if (scrollAreaRef.current) {
|
|
||||||
setShowScrollToTop(scrollAreaRef.current.scrollTop > 600)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const scrollArea = scrollAreaRef.current
|
|
||||||
scrollArea?.addEventListener('scroll', handleScroll)
|
|
||||||
return () => {
|
|
||||||
scrollArea?.removeEventListener('scroll', handleScroll)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary-2"
|
variant="secondary-2"
|
||||||
className={cn(
|
className={cn(
|
||||||
`absolute bottom-8 right-2 rounded-full w-10 h-10 p-0 hover:text-background transition-transform ${showScrollToTop ? '' : 'translate-y-20'}`,
|
`absolute bottom-2 right-2 rounded-full w-11 h-11 p-0 hover:text-background transition-transform ${visible ? '' : 'translate-y-14'}`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={handleScrollToTop}
|
onClick={handleScrollToTop}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { SearchDialog } from '../SearchDialog'
|
||||||
export default function RefreshButton({
|
export default function RefreshButton({
|
||||||
variant = 'titlebar'
|
variant = 'titlebar'
|
||||||
}: {
|
}: {
|
||||||
variant?: 'titlebar' | 'sidebar'
|
variant?: 'titlebar' | 'sidebar' | 'small-screen-titlebar'
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||||
import { Moon, Sun, SunMoon } from 'lucide-react'
|
import { Moon, Sun, SunMoon } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export default function ThemeToggle() {
|
export default function ThemeToggle({
|
||||||
|
variant = 'titlebar'
|
||||||
|
}: {
|
||||||
|
variant?: 'titlebar' | 'small-screen-titlebar'
|
||||||
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { themeSetting, setThemeSetting } = useTheme()
|
const { themeSetting, setThemeSetting } = useTheme()
|
||||||
|
|
||||||
|
|
@ -11,8 +15,8 @@ export default function ThemeToggle() {
|
||||||
<>
|
<>
|
||||||
{themeSetting === 'system' ? (
|
{themeSetting === 'system' ? (
|
||||||
<Button
|
<Button
|
||||||
variant="titlebar"
|
variant={variant}
|
||||||
size="titlebar"
|
size={variant}
|
||||||
onClick={() => setThemeSetting('light')}
|
onClick={() => setThemeSetting('light')}
|
||||||
title={t('switch to light theme')}
|
title={t('switch to light theme')}
|
||||||
>
|
>
|
||||||
|
|
@ -20,8 +24,8 @@ export default function ThemeToggle() {
|
||||||
</Button>
|
</Button>
|
||||||
) : themeSetting === 'light' ? (
|
) : themeSetting === 'light' ? (
|
||||||
<Button
|
<Button
|
||||||
variant="titlebar"
|
variant={variant}
|
||||||
size="titlebar"
|
size={variant}
|
||||||
onClick={() => setThemeSetting('dark')}
|
onClick={() => setThemeSetting('dark')}
|
||||||
title={t('switch to dark theme')}
|
title={t('switch to dark theme')}
|
||||||
>
|
>
|
||||||
|
|
@ -29,8 +33,8 @@ export default function ThemeToggle() {
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
variant="titlebar"
|
variant={variant}
|
||||||
size="titlebar"
|
size={variant}
|
||||||
onClick={() => setThemeSetting('system')}
|
onClick={() => setThemeSetting('system')}
|
||||||
title={t('switch to system theme')}
|
title={t('switch to system theme')}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,23 @@ import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||||
|
|
||||||
export function Titlebar({
|
export function Titlebar({
|
||||||
children,
|
children,
|
||||||
className
|
className,
|
||||||
|
visible = true
|
||||||
}: {
|
}: {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
visible?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
if (isMacOS() && isSmallScreen) {
|
if (isMacOS() && isSmallScreen) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-0 w-full z-50 bg-background/80 backdrop-blur-md font-semibold">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute top-0 w-full z-50 bg-background/80 backdrop-blur-md font-semibold transition-transform',
|
||||||
|
visible ? '' : '-translate-y-full'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="draggable h-9 w-full" />
|
<div className="draggable h-9 w-full" />
|
||||||
<div className={cn('h-11 flex gap-1 items-center', className)}>{children}</div>
|
<div className={cn('h-11 flex gap-1 items-center', className)}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,7 +30,8 @@ export function Titlebar({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'draggable absolute top-0 w-full h-9 z-50 bg-background/80 backdrop-blur-md flex items-center font-semibold gap-1 px-2',
|
'draggable absolute top-0 w-full h-9 max-sm:h-11 z-50 bg-background/80 backdrop-blur-md flex items-center font-semibold gap-1 px-2 duration-700 transition-transform',
|
||||||
|
visible ? '' : '-translate-y-full',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ const buttonVariants = cva(
|
||||||
ghost: 'text-muted-foreground hover:bg-accent hover:text-foreground',
|
ghost: 'text-muted-foreground hover:bg-accent hover:text-foreground',
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
titlebar: 'non-draggable hover:bg-accent hover:text-accent-foreground',
|
titlebar: 'non-draggable hover:bg-accent hover:text-accent-foreground',
|
||||||
sidebar: 'non-draggable hover:bg-accent hover:text-accent-foreground'
|
sidebar: 'non-draggable hover:bg-accent hover:text-accent-foreground',
|
||||||
|
'small-screen-titlebar': 'non-draggable hover:bg-accent hover:text-accent-foreground'
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-8 rounded-lg px-3',
|
default: 'h-8 rounded-lg px-3',
|
||||||
|
|
@ -25,7 +26,8 @@ const buttonVariants = cva(
|
||||||
lg: 'h-10 px-4 py-2',
|
lg: 'h-10 px-4 py-2',
|
||||||
icon: 'h-8 w-8 rounded-full',
|
icon: 'h-8 w-8 rounded-full',
|
||||||
titlebar: 'h-7 w-7 rounded-full',
|
titlebar: 'h-7 w-7 rounded-full',
|
||||||
sidebar: 'w-full flex py-2 px-4 rounded-full justify-start gap-4 text-lg font-semibold'
|
sidebar: 'w-full flex py-2 px-4 rounded-full justify-start gap-4 text-lg font-semibold',
|
||||||
|
'small-screen-titlebar': 'h-8 w-8 rounded-full'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const CommandDialog = ({
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className={cn(
|
className={cn(
|
||||||
'overflow-hidden p-0 shadow-lg top-4 translate-y-0 data-[state=closed]:slide-out-to-top-0 data-[state=open]:slide-in-from-top-0',
|
'overflow-hidden p-0 shadow-lg top-4 translate-y-0 data-[state=closed]:slide-out-to-top-4 data-[state=open]:slide-in-from-top-4',
|
||||||
classNames?.content
|
classNames?.content
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const DialogContent = React.forwardRef<
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 sm:border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||||
'z-50 min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
'z-50 min-w-[8rem] overflow-hidden rounded-lg border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
collisionPadding={10}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</DropdownMenuPrimitive.Portal>
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,18 @@ import RefreshButton from '@renderer/components/RefreshButton'
|
||||||
import RelaySettingsButton from '@renderer/components/RelaySettingsButton'
|
import RelaySettingsButton from '@renderer/components/RelaySettingsButton'
|
||||||
import ScrollToTopButton from '@renderer/components/ScrollToTopButton'
|
import ScrollToTopButton from '@renderer/components/ScrollToTopButton'
|
||||||
import SearchButton from '@renderer/components/SearchButton'
|
import SearchButton from '@renderer/components/SearchButton'
|
||||||
|
import ThemeToggle from '@renderer/components/ThemeToggle'
|
||||||
import { Titlebar } from '@renderer/components/Titlebar'
|
import { Titlebar } from '@renderer/components/Titlebar'
|
||||||
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||||
import { isMacOS } from '@renderer/lib/env'
|
import { isMacOS } from '@renderer/lib/env'
|
||||||
import { cn } from '@renderer/lib/utils'
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||||
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||||
|
|
||||||
const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode }, ref) => {
|
const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode }, ref) => {
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null)
|
const scrollAreaRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [visible, setVisible] = useState(true)
|
||||||
|
const [lastScrollTop, setLastScrollTop] = useState(0)
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
ref,
|
ref,
|
||||||
|
|
@ -24,17 +27,36 @@ const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = scrollAreaRef.current?.scrollTop || 0
|
||||||
|
if (scrollTop > lastScrollTop) {
|
||||||
|
setVisible(false)
|
||||||
|
} else {
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
setLastScrollTop(scrollTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollArea = scrollAreaRef.current
|
||||||
|
scrollArea?.addEventListener('scroll', handleScroll)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollArea?.removeEventListener('scroll', handleScroll)
|
||||||
|
}
|
||||||
|
}, [lastScrollTop])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
ref={scrollAreaRef}
|
ref={scrollAreaRef}
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
scrollBarClassName="pt-9 max-sm:pt-0 xl:pt-0"
|
scrollBarClassName="pt-9 max-sm:pt-0 xl:pt-0"
|
||||||
>
|
>
|
||||||
<PrimaryPageTitlebar />
|
<PrimaryPageTitlebar visible={visible} />
|
||||||
<div className={cn('sm:px-4 pb-4 pt-11 xl:pt-4', isMacOS() ? 'max-sm:pt-20' : 'max-sm:pt-9')}>
|
<div className={cn('sm:px-4 pb-4 pt-11 xl:pt-4', isMacOS() ? 'max-sm:pt-20' : '')}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<ScrollToTopButton scrollAreaRef={scrollAreaRef} />
|
<ScrollToTopButton scrollAreaRef={scrollAreaRef} visible={visible} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -45,18 +67,24 @@ export type TPrimaryPageLayoutRef = {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrimaryPageTitlebar() {
|
function PrimaryPageTitlebar({ visible = true }: { visible?: boolean }) {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
return (
|
return (
|
||||||
<Titlebar className="justify-between px-4">
|
<Titlebar
|
||||||
<div className="text-2xl font-extrabold font-mono">Jumble</div>
|
className="justify-between px-4 transition-transform duration-500"
|
||||||
<div className="flex gap-2 items-center">
|
visible={visible}
|
||||||
<SearchButton />
|
>
|
||||||
<PostButton />
|
<div className="flex gap-1 items-center">
|
||||||
<RelaySettingsButton />
|
<div className="text-2xl font-extrabold font-mono">Jumble</div>
|
||||||
<AccountButton />
|
<ThemeToggle variant="small-screen-titlebar" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
<SearchButton variant="small-screen-titlebar" />
|
||||||
|
<PostButton variant="small-screen-titlebar" />
|
||||||
|
<RelaySettingsButton variant="small-screen-titlebar" />
|
||||||
|
<AccountButton variant="small-screen-titlebar" />
|
||||||
</div>
|
</div>
|
||||||
</Titlebar>
|
</Titlebar>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ScrollArea } from '@renderer/components/ui/scroll-area'
|
||||||
import { isMacOS } from '@renderer/lib/env'
|
import { isMacOS } from '@renderer/lib/env'
|
||||||
import { cn } from '@renderer/lib/utils'
|
import { cn } from '@renderer/lib/utils'
|
||||||
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@renderer/providers/ScreenSizeProvider'
|
||||||
import { useRef } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
export default function SecondaryPageLayout({
|
export default function SecondaryPageLayout({
|
||||||
children,
|
children,
|
||||||
|
|
@ -18,30 +18,58 @@ export default function SecondaryPageLayout({
|
||||||
hideBackButton?: boolean
|
hideBackButton?: boolean
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const scrollAreaRef = useRef<HTMLDivElement>(null)
|
const scrollAreaRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [visible, setVisible] = useState(true)
|
||||||
|
const [lastScrollTop, setLastScrollTop] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
const scrollTop = scrollAreaRef.current?.scrollTop || 0
|
||||||
|
if (scrollTop > lastScrollTop) {
|
||||||
|
setVisible(false)
|
||||||
|
} else {
|
||||||
|
setVisible(true)
|
||||||
|
}
|
||||||
|
setLastScrollTop(scrollTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollArea = scrollAreaRef.current
|
||||||
|
scrollArea?.addEventListener('scroll', handleScroll)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollArea?.removeEventListener('scroll', handleScroll)
|
||||||
|
}
|
||||||
|
}, [lastScrollTop])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea ref={scrollAreaRef} className="h-full" scrollBarClassName="pt-9">
|
<ScrollArea ref={scrollAreaRef} className="h-full" scrollBarClassName="pt-9">
|
||||||
<SecondaryPageTitlebar content={titlebarContent} hideBackButton={hideBackButton} />
|
<SecondaryPageTitlebar
|
||||||
|
content={titlebarContent}
|
||||||
|
hideBackButton={hideBackButton}
|
||||||
|
visible={visible}
|
||||||
|
/>
|
||||||
<div className={cn('sm:px-4 pb-4 pt-11 w-full h-full', isMacOS() ? 'max-sm:pt-20' : '')}>
|
<div className={cn('sm:px-4 pb-4 pt-11 w-full h-full', isMacOS() ? 'max-sm:pt-20' : '')}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
<ScrollToTopButton scrollAreaRef={scrollAreaRef} />
|
<ScrollToTopButton scrollAreaRef={scrollAreaRef} visible={visible} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SecondaryPageTitlebar({
|
export function SecondaryPageTitlebar({
|
||||||
content,
|
content,
|
||||||
hideBackButton = false
|
hideBackButton = false,
|
||||||
|
visible = true
|
||||||
}: {
|
}: {
|
||||||
content?: React.ReactNode
|
content?: React.ReactNode
|
||||||
hideBackButton?: boolean
|
hideBackButton?: boolean
|
||||||
|
visible?: boolean
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
return (
|
return (
|
||||||
<Titlebar className="pl-2">
|
<Titlebar className="pl-2" visible={visible}>
|
||||||
<BackButton hide={hideBackButton} />
|
<BackButton hide={hideBackButton} variant="small-screen-titlebar" />
|
||||||
<div className="truncate text-lg">{content}</div>
|
<div className="truncate text-lg">{content}</div>
|
||||||
</Titlebar>
|
</Titlebar>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ export default function NotePage({ id }: { id?: string }) {
|
||||||
<ParentNote key={`parent-note-${event.id}`} eventId={parentEventId} />
|
<ParentNote key={`parent-note-${event.id}`} eventId={parentEventId} />
|
||||||
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
<Note key={`note-${event.id}`} event={event} fetchNoteStats />
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-4" />
|
<Separator className="mb-2 mt-4" />
|
||||||
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="max-sm:px-2" />
|
<ReplyNoteList key={`reply-note-list-${event.id}`} event={event} className="max-sm:px-2" />
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue