0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

Improved posts design and icon button hover states (#21754)

close https://linear.app/ghost/issue/AP-599/make-hover-and-click-states-for-buttons-consistent, https://linear.app/ghost/issue/AP-616/improve-feed-view-design

- Updated design for posts shown in the feed, reply and drawer views
- Consistent hover and click states for icon buttons
- Fixed inbox view posts overlapping with the sidebar suggestions
This commit is contained in:
Djordje Vlaisavljevic 2024-11-28 13:47:56 +00:00 committed by GitHub
parent 6a70ba8895
commit 894e1ed798
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 120 additions and 127 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@tryghost/admin-x-activitypub",
"version": "0.3.24",
"version": "0.3.25",
"license": "MIT",
"repository": {
"type": "git",

View file

@ -86,7 +86,7 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
</div>
) : activities.length > 0 ? (
<>
<div className={`mx-auto flex min-h-[calc(100dvh_-_117px)] items-start gap-9`}>
<div className={`mx-auto flex min-h-[calc(100dvh_-_117px)] items-start gap-11`}>
<div className='flex w-full min-w-0 flex-col items-center'>
<div className={`flex w-full min-w-0 flex-col items-start ${layout === 'inbox' ? 'xxxl:max-w-[800px]' : 'max-w-[500px]'}`}>
{layout === 'feed' && <div className='relative mx-[-12px] mb-4 mt-10 flex w-[calc(100%+24px)] items-center p-3'>
@ -124,8 +124,7 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
</ul>
</div>
</div>
<div className='sticky top-[133px] ml-auto w-full max-w-[300px] max-lg:hidden xxxl:sticky xxxl:right-[40px]'>
{/* <Icon className='mb-2' colorClass='text-blue-500' name='comment' size='md' /> */}
<div className={`sticky top-[133px] ml-auto w-full max-w-[300px] max-lg:hidden xxxl:sticky xxxl:right-[40px]`}>
<h2 className='mb-2 text-lg font-semibold'>This is your {layout === 'inbox' ? 'inbox' : 'feed'}</h2>
<p className='mb-6 border-b border-grey-200 pb-6 text-grey-700'>You&apos;ll find {layout === 'inbox' ? 'long-form content' : 'short posts and updates'} from the accounts you follow here.</p>
<h2 className='mb-2 text-lg font-semibold'>You might also like</h2>
@ -135,7 +134,6 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
<ul className='grow'>
{suggested.map((profile, index) => {
const actor = profile.actor;
// const isFollowing = profile.isFollowing;
return (
<React.Fragment key={actor.id}>
<li key={actor.id}>
@ -149,14 +147,6 @@ const Inbox: React.FC<InboxProps> = ({layout}) => {
<span className='block w-full truncate font-bold text-black'>{getName(actor)}</span>
<span className='block w-full truncate text-sm text-grey-600'>{getUsername(actor)}</span>
</div>
{/* <FollowButton
className='ml-auto'
following={isFollowing}
handle={getUsername(actor)}
type='link'
onFollow={() => updateSuggestedProfile(actor.id!, {isFollowing: true})}
onUnfollow={() => updateSuggestedProfile(actor.id!, {isFollowing: false})}
/> */}
</ActivityItem>
</li>
{index < suggested.length - 1 && <Separator />}

View file

@ -2015,9 +2015,9 @@ Search LOGO Login Subscribe
.gh-article-excerpt {
margin-top: 16px;
font-size: 2.1rem;
line-height: 1.3;
letter-spacing: -0.63px;
font-size: 2rem;
line-height: 1.4;
letter-spacing: -0.017em;
text-wrap: pretty;
}
@ -2120,9 +2120,10 @@ created within the Ghost editor. The main content handles
headings, text, images and lists. We deal with cards lower down. */
.gh-content {
font-size: var(--content-font-size, 1.7rem);
letter-spacing: -0.01em;
font-size: 17px;
overflow-x: hidden;
letter-spacing: -0.013em;
line-height: 1.4117647059;
}
/* Default vertical spacing */
@ -2580,7 +2581,9 @@ figcaption a {
}
.author-template .gh-article-title {
font-size: 2.9rem;
font-size: 3rem;
letter-spacing: -0.021em;
line-height: 1.4;
}
.gh-author-meta {

View file

@ -136,7 +136,7 @@ const ArticleBody: React.FC<{heading: string, image: string|undefined, excerpt:
}, [htmlContent]);
return (
<div className='w-full pb-10'>
<div className='w-full pb-6'>
<iframe
ref={iframeRef}
id='gh-ap-article-iframe'
@ -298,11 +298,11 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
<div className='col-[2/3] flex grow items-center justify-center px-8 text-center'>
</div>
<div className='col-[3/4] flex items-center justify-end space-x-6'>
<Button className='flex w-10 items-center justify-center' icon='close' size='sm' unstyled onClick={() => modal.remove()}/>
<Button className='transition-color flex h-10 w-10 items-center justify-center rounded-full bg-white hover:bg-grey-100' icon='close' size='sm' unstyled onClick={() => modal.remove()}/>
</div>
</div>
</div>
<div className='grow overflow-y-auto'>
<div className='mx-auto max-w-[580px] pb-10 pt-5'>
{activityThreadParents.map((item) => {
@ -351,19 +351,21 @@ const ArticleModal: React.FC<ArticleModalProps> = ({
html={object.content}
image={object?.image}
/>
<FeedItemStats
commentCount={object.replyCount ?? 0}
layout={'modal'}
likeCount={1}
object={object}
onCommentClick={() => {
repliesRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}}
onLikeClick={onLikeClick}
/>
<div className='ml-[-7px]'>
<FeedItemStats
commentCount={object.replyCount ?? 0}
layout={'modal'}
likeCount={1}
object={object}
onCommentClick={() => {
repliesRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}}
onLikeClick={onLikeClick}
/>
</div>
</div>
)}

View file

@ -71,7 +71,7 @@ export function renderFeedAttachment(object: ObjectProperties, layout: string) {
case 'image/jpeg':
case 'image/png':
case 'image/gif':
return <img alt='attachment' className='mt-3 rounded-md outline outline-1 -outline-offset-1 outline-black/10' src={attachment.url} />;
return <img alt='attachment' className='mt-3 max-h-[420px] rounded-md outline outline-1 -outline-offset-1 outline-black/10' src={attachment.url} />;
case 'video/mp4':
case 'video/webm':
return <div className='relative mb-4 mt-3'>
@ -163,7 +163,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
const date = new Date(object?.published ?? new Date());
const [isCopied, setIsCopied] = useState(false);
const [, setIsCopied] = useState(false);
const onLikeClick = () => {
// Do API req or smth
@ -218,12 +218,12 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
const UserMenuTrigger = (
<Button
className={`relative z-[9998] ml-auto flex h-5 w-5 items-center justify-center self-start hover:opacity-60 ${isCopied ? 'bump' : ''}`}
className={`transition-color relative z-[9998] flex h-[34px] w-[34px] items-center justify-center rounded-full bg-white hover:bg-grey-100 ${(layout === 'feed' || layout === 'modal') && 'ml-auto'}`}
hideLabel={true}
icon='dotdotdot'
iconColorClass={`${layout === 'inbox' ? 'text-grey-900' : 'text-grey-500'}`}
iconColorClass={`${layout === 'inbox' ? 'text-grey-900 w-[12px] h-[12px]' : 'text-grey-500 w-[16px] h-[16px]'}`}
id='more'
size='sm'
size='md'
unstyled={true}
/>
);
@ -232,13 +232,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
return (
<>
{object && (
<div className={`group/article relative cursor-pointer py-6`} data-layout='feed' data-object-id={object.id} onClick={onClick}>
<div className={`group/article relative cursor-pointer pb-[18px] pt-6`} data-layout='feed' data-object-id={object.id} onClick={onClick}>
{(type === 'Announce' && object.type === 'Note') && <div className='z-10 mb-2 flex items-center gap-3 text-grey-700'>
<div className='z-10 flex w-10 justify-end'><Icon colorClass='text-grey-700' name='reload' size={'sm'}></Icon></div>
<span className='z-10'>{actor.name} reposted</span>
</div>}
<div className={`border-1 flex flex-col gap-2.5`} data-test-activity>
<div className='flex min-w-0 items-center gap-3'>
<div className='relative z-30 flex min-w-0 items-center gap-3'>
<APAvatar author={author}/>
<div className='flex min-w-0 flex-col gap-0.5'>
<span className='min-w-0 truncate break-all font-semibold leading-[normal]' data-test-activity-heading>{author.name}</span>
@ -247,13 +247,14 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<div className='ml-1 leading-tight before:mr-1 before:content-["·"]' title={`${timestamp}`}>{renderTimestamp(object)}</div>
</div>
</div>
<Menu items={menuItems} open={menuIsOpen} position='end' setOpen={setMenuIsOpen} trigger={UserMenuTrigger}/>
</div>
<div className={`relative col-start-2 col-end-3 w-full gap-4`}>
<div className='flex flex-col'>
<div className=''>
{(object.type === 'Article') && renderFeedAttachment(object, layout)}
{object.name && <Heading className='my-1 text-pretty leading-tight' level={5} data-test-activity-heading>{object.name}</Heading>}
{(object.preview && object.type === 'Article') ? <div className='line-clamp-3 leading-tight'>{object.preview.content}</div> : <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>}
{(object.preview && object.type === 'Article') ? <div className='line-clamp-3 leading-tight'>{object.preview.content}</div> : <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty leading-[1.4] tracking-[-0.006em] text-grey-900'></div>}
{(object.type === 'Note') && renderFeedAttachment(object, layout)}
{(object.type === 'Article') && <Button
className={`mt-3 self-start text-grey-900 transition-all hover:opacity-60`}
@ -264,7 +265,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
size='md'
/>}
</div>
<div className='space-between relative z-[30] mt-5 flex'>
<div className='space-between relative z-[30] ml-[-7px] mt-1 flex'>
<FeedItemStats
commentCount={commentCount}
layout={layout}
@ -273,7 +274,6 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
onCommentClick={onCommentClick}
onLikeClick={onLikeClick}
/>
<Menu items={menuItems} open={menuIsOpen} position='end' setOpen={setMenuIsOpen} trigger={UserMenuTrigger}/>
</div>
</div>
</div>
@ -292,7 +292,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<div className='z-10 flex w-10 justify-end'><Icon colorClass='text-grey-700' name='reload' size={'sm'}></Icon></div>
<span className='z-10'>{actor.name} reposted</span>
</div>}
<div className={`z-10 -my-1 grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-3 pb-4 pt-5`} data-test-activity>
<div className={`z-10 -my-1 grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-3 pb-3 pt-4`} data-test-activity>
{(showHeader) && <><div className='relative z-10 pt-[3px]'>
<APAvatar author={author}/>
</div>
@ -308,9 +308,9 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<div className={`relative z-10 col-start-1 col-end-3 w-full gap-4`}>
<div className='flex flex-col'>
{object.name && <Heading className='mb-1 leading-tight' level={4} data-test-activity-heading>{object.name}</Heading>}
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.8rem] text-grey-900'></div>
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.6rem] tracking-[-0.011em] text-grey-900'></div>
{renderFeedAttachment(object, layout)}
<div className='space-between mt-5 flex'>
<div className='space-between ml-[-7px] mt-3 flex'>
<FeedItemStats
commentCount={commentCount}
layout={layout}
@ -341,43 +341,47 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<div className='z-10 flex w-10 justify-end'><Icon colorClass='text-grey-700' name='reload' size={'sm'}></Icon></div>
<span className='z-10'>{actor.name} reposted</span>
</div>}
<div className={`border-1 z-10 -my-1 grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-x-3 gap-y-2 border-b-grey-200`} data-test-activity>
<div className='relative z-10 min-w-0 pt-[3px]'>
<div className={`border-1 z-10 flex items-start gap-3 border-b-grey-200`} data-test-activity>
<div className='relative z-10 pt-[3px]'>
<APAvatar author={author}/>
</div>
<div className='relative z-10 flex w-full min-w-0 flex-col overflow-visible text-[1.5rem]'>
<div className='flex'>
<span className='min-w-0 truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
<div>{renderTimestamp(object)}</div>
<div className='flex w-full min-w-0 flex-col gap-2'>
<div className='flex w-full items-center justify-between'>
<div className='relative z-10 flex w-full min-w-0 flex-col overflow-visible'>
<div className='flex'>
<span className='min-w-0 truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
<div>{renderTimestamp(object)}</div>
</div>
<div className='flex'>
<span className='truncate text-grey-700'>{getUsername(author)}</span>
</div>
</div>
<Menu items={menuItems} open={menuIsOpen} position='end' setOpen={setMenuIsOpen} trigger={UserMenuTrigger}/>
</div>
<div className='flex'>
<span className='truncate text-grey-700'>{getUsername(author)}</span>
</div>
</div>
<div className={`relative z-10 col-start-2 col-end-3 w-full gap-4`}>
<div className='flex flex-col'>
{(object.type === 'Article') && renderFeedAttachment(object, layout)}
{object.name && <Heading className='my-1 text-pretty leading-tight' level={5} data-test-activity-heading>{object.name}</Heading>}
{(object.preview && object.type === 'Article') ? <div className='line-clamp-3 leading-tight'>{object.preview.content}</div> : <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty text-[1.5rem] text-grey-900'></div>}
{(object.type === 'Note') && renderFeedAttachment(object, layout)}
{(object.type === 'Article') && <Button
className={`mt-3 self-start text-grey-900 transition-all hover:opacity-60`}
color='grey'
fullWidth={true}
id='read-more'
label='Read more'
size='md'
/>}
<div className='space-between mt-5 flex'>
<FeedItemStats
commentCount={commentCount}
layout={layout}
likeCount={1}
object={object}
onCommentClick={onCommentClick}
onLikeClick={onLikeClick}
/>
<Menu items={menuItems} open={menuIsOpen} position='end' setOpen={setMenuIsOpen} trigger={UserMenuTrigger}/>
<div className={`relative z-10 col-start-2 col-end-3 w-full gap-4`}>
<div className='flex flex-col'>
{(object.type === 'Article') && renderFeedAttachment(object, layout)}
{object.name && <Heading className='my-1 text-pretty leading-tight' level={5} data-test-activity-heading>{object.name}</Heading>}
{(object.preview && object.type === 'Article') ? <div className='line-clamp-3 leading-tight'>{object.preview.content}</div> : <div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content text-pretty tracking-[-0.006em] text-grey-900'></div>}
{(object.type === 'Note') && renderFeedAttachment(object, layout)}
{(object.type === 'Article') && <Button
className={`mt-3 self-start text-grey-900 transition-all hover:opacity-60`}
color='grey'
fullWidth={true}
id='read-more'
label='Read more'
size='md'
/>}
<div className='space-between ml-[-7px] mt-2 flex'>
<FeedItemStats
commentCount={commentCount}
layout={layout}
likeCount={1}
object={object}
onCommentClick={onCommentClick}
onLikeClick={onLikeClick}
/>
</div>
</div>
</div>
</div>
@ -393,7 +397,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<>
{object && (
<div className='group/article relative -mx-6 -my-px flex min-h-[112px] min-w-0 cursor-pointer items-center justify-between rounded-md p-6 hover:bg-grey-75' data-layout='inbox' data-object-id={object.id} onClick={onClick}>
<div>
<div className='min-w-0'>
<div className='z-10 mb-1.5 flex w-full min-w-0 items-center gap-1.5 text-base group-hover/article:border-transparent'>
<APAvatar author={author} size='2xs'/>
<span className='min-w-0 truncate break-all font-medium text-grey-900' title={getUsername(author)} data-test-activity-heading>{author.name}</span>
@ -411,7 +415,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
<div dangerouslySetInnerHTML={({__html: stripHtml(object.preview?.content ?? object.content)})} className='ap-note-content line-clamp-2 w-full max-w-[600px] text-pretty text-base leading-normal text-grey-700'></div>
<span className='mt-1 shrink-0 whitespace-nowrap leading-none text-grey-700'>{object.content && `${getReadingTime(object.content)}`}</span>
</div>
<div className='invisible absolute right-4 top-1/2 z-[49] flex -translate-y-1/2 flex-col gap-3 rounded-full bg-white px-2 py-3 shadow-md group-hover/article:visible'>
<div className='invisible absolute right-4 top-1/2 z-[49] flex -translate-y-1/2 flex-col rounded-full bg-white px-1 py-2 shadow-md group-hover/article:visible'>
<FeedItemStats
commentCount={commentCount}
layout={layout}

View file

@ -20,60 +20,54 @@ const FeedItemStats: React.FC<FeedItemStatsProps> = ({
onLikeClick,
onCommentClick
}) => {
const [isClicked, setIsClicked] = useState(false);
const [isLiked, setIsLiked] = useState(object.liked);
const likeMutation = useLikeMutationForUser('index');
const unlikeMutation = useUnlikeMutationForUser('index');
const handleLikeClick = async (e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
setIsClicked(true);
if (!isLiked) {
likeMutation.mutate(object.id);
} else {
unlikeMutation.mutate(object.id);
}
setIsLiked(!isLiked);
onLikeClick();
setTimeout(() => setIsClicked(false), 300);
};
return (<div className={`flex ${(layout === 'inbox') ? 'flex-col gap-3' : 'gap-5'}`}>
<div className='flex gap-1'>
<Button
className={`self-start text-grey-900 transition-opacity hover:opacity-60 ${isClicked ? 'bump' : ''} ${isLiked ? 'ap-red-heart text-red *:!fill-red hover:text-red' : ''}`}
hideLabel={true}
icon='heart'
id='like'
size='md'
unstyled={true}
onClick={(e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
if (e) {
handleLikeClick(e);
}
}}
/>
{isLiked && (layout !== 'inbox') && <span className={`text-grey-900`}>{new Intl.NumberFormat().format(likeCount)}</span>}
</div>
<div className='flex gap-1'>
<Button
className={`self-start text-grey-900 hover:opacity-60 ${isClicked ? 'bump' : ''}`}
hideLabel={true}
icon='comment'
id='comment'
size='md'
unstyled={true}
onClick={(e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
onCommentClick();
}}
/>
{commentCount > 0 && (layout !== 'inbox') && (
<span className={`text-grey-900`}>{new Intl.NumberFormat().format(commentCount)}</span>
)}
</div>
const buttonClassName = `transition-color flex p-2 items-center justify-center rounded-full bg-white leading-none text-grey-900 hover:bg-grey-100`;
return (<div className={`flex ${(layout === 'inbox') ? 'flex-col' : 'gap-1'}`}>
<Button
className={buttonClassName}
hideLabel={!isLiked || (layout === 'inbox')}
icon='heart'
iconColorClass={`w-[18px] h-[18px] ${isLiked && 'ap-red-heart text-red *:!fill-red hover:text-red'}`}
id='like'
label={new Intl.NumberFormat().format(likeCount)}
size='md'
unstyled={true}
onClick={(e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
if (e) {
handleLikeClick(e);
}
}}
/>
<Button
className={buttonClassName}
hideLabel={commentCount === 0 || (layout === 'inbox')}
icon='comment'
iconColorClass='w-[18px] h-[18px]'
id='comment'
label={new Intl.NumberFormat().format(commentCount)}
size='md'
unstyled={true}
onClick={(e?: React.MouseEvent<HTMLElement>) => {
e?.stopPropagation();
onCommentClick();
}}
/>
</div>);
};

View file

@ -137,7 +137,7 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
{hint}
</div>
</FormPrimitive.Root>
<div className='absolute bottom-[3px] right-[9px] flex space-x-4 transition-[opacity] duration-150'>
<div className='absolute bottom-[3px] right-0 flex space-x-4 transition-[opacity] duration-150'>
<Button color='black' disabled={buttonDisabled} id='post' label='Post' loading={replyMutation.isLoading} size='md' onMouseDown={handleClick} />
</div>
</div>

View file

@ -18,7 +18,7 @@
}
}
.bump {
button:active svg {
animation: bump 0.3s ease-in-out;
}
@ -50,7 +50,7 @@
}
.ap-note-content p+p {
margin-top: 1.5rem !important;
margin-top: 1.2rem !important;
}
.ap-likes .invisible {
@ -64,4 +64,4 @@
.ap-textarea {
field-sizing: content;
}
}