0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-27 22:49:56 -05:00

Improved Inbox view UI (#21600)

ref https://linear.app/ghost/issue/AP-521/improve-inbox-view-alignmentstructure

- Improved the layout, spacing and typography of Inbox view
- Removed attachment counters for images in Inbox view
- Added a very very small variant of `APAvatar`
This commit is contained in:
Djordje Vlaisavljevic 2024-11-13 11:45:37 +00:00 committed by GitHub
parent ead408ed20
commit 5863c40306
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 30 additions and 38 deletions

View file

@ -76,9 +76,9 @@ const Inbox: React.FC<InboxProps> = ({}) => {
</div> </div>
) : activities.length > 0 ? ( ) : activities.length > 0 ? (
<> <>
<div className={`mx-auto flex items-start ${layout === 'inbox' ? 'max-w-6xl gap-14' : 'gap-8'}`}> <div className={`mx-auto flex items-start gap-8`}>
<div className='flex w-full min-w-0 items-start'> <div className='flex w-full min-w-0 items-start'>
<ul className={`mx-auto flex ${layout === 'inbox' ? 'w-full max-w-full' : 'max-w-[500px]'} flex-col`}> <ul className={`mx-auto flex w-full flex-col ${layout === 'inbox' ? 'xxxl:max-w-[800px]' : 'max-w-[500px]'}`}>
{activities.map((activity, index) => ( {activities.map((activity, index) => (
<li <li
key={activity.id} key={activity.id}
@ -106,7 +106,7 @@ const Inbox: React.FC<InboxProps> = ({}) => {
)} )}
</ul> </ul>
</div> </div>
<div className={`sticky top-[135px] ml-auto w-full max-w-[300px] max-lg:hidden ${layout === 'inbox' ? '' : ' xxxl:fixed xxxl:right-[40px]'}`}> <div className='sticky top-[135px] ml-auto w-full max-w-[300px] max-lg:hidden xxxl:sticky xxxl:right-[40px]'>
<h2 className='mb-2 text-lg font-semibold'>You might also like...</h2> <h2 className='mb-2 text-lg font-semibold'>You might also like...</h2>
{isLoadingSuggested ? ( {isLoadingSuggested ? (
<LoadingIndicator size="sm" /> <LoadingIndicator size="sm" />

View file

@ -5,6 +5,7 @@ import {Button, Heading, Icon, Menu, MenuItem, showToast} from '@tryghost/admin-
import APAvatar from '../global/APAvatar'; import APAvatar from '../global/APAvatar';
import FeedItemStats from './FeedItemStats'; import FeedItemStats from './FeedItemStats';
import clsx from 'clsx';
import getRelativeTimestamp from '../../utils/get-relative-timestamp'; import getRelativeTimestamp from '../../utils/get-relative-timestamp';
import getUsername from '../../utils/get-username'; import getUsername from '../../utils/get-username';
import stripHtml from '../../utils/strip-html'; import stripHtml from '../../utils/strip-html';
@ -83,7 +84,7 @@ export function renderFeedAttachment(object: ObjectProperties, layout: string) {
</div>; </div>;
default: default:
if (object.image) { if (object.image) {
return <img alt='attachment' className='my-3 max-h-[280px] w-full rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10' src={object.image} />; return <img alt='attachment' className='my-3 max-h-[280px] w-full rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/[0.05]' src={object.image} />;
} }
return null; return null;
} }
@ -92,20 +93,16 @@ export function renderFeedAttachment(object: ObjectProperties, layout: string) {
function renderInboxAttachment(object: ObjectProperties) { function renderInboxAttachment(object: ObjectProperties) {
const attachment = getAttachment(object); const attachment = getAttachment(object);
const videoAttachmentStyles = 'ml-8 shrink-0 rounded-md h-[80px] w-[120px] relative';
const imageAttachmentStyles = clsx('object-cover outline outline-1 -outline-offset-1 outline-black/[0.05]', videoAttachmentStyles);
if (!attachment) { if (!attachment) {
return null; return null;
} }
if (Array.isArray(attachment)) { if (Array.isArray(attachment)) {
const attachmentCount = attachment.length;
return ( return (
<div className='min-w-[120px]'> <img className={imageAttachmentStyles} src={attachment[0].url} />
<div className='relative'>
<img className={`h-[80px] w-[120px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={attachment[0].url} />
<div className='absolute bottom-1 right-1 z-10 rounded-full border border-[rgba(255,255,255,0.25)] bg-black px-2 py-0.5 font-semibold text-white'>+ {attachmentCount - 1}</div>
</div>
</div>
); );
} }
@ -114,14 +111,12 @@ function renderInboxAttachment(object: ObjectProperties) {
case 'image/png': case 'image/png':
case 'image/gif': case 'image/gif':
return ( return (
<div className='min-w-[120px]'> <img className={imageAttachmentStyles} src={attachment.url} />
<img className={`h-[80px] w-[120px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={attachment.url} />
</div>
); );
case 'video/mp4': case 'video/mp4':
case 'video/webm': case 'video/webm':
return ( return (
<div className='relative h-[80px]'> <div className={videoAttachmentStyles}>
<video className='h-[80px] w-full rounded object-cover' src={attachment.url} /> <video className='h-[80px] w-full rounded object-cover' src={attachment.url} />
<div className='absolute inset-0 rounded bg-grey-900 opacity-50'></div> <div className='absolute inset-0 rounded bg-grey-900 opacity-50'></div>
<div className='absolute inset-0 flex items-center justify-center'> <div className='absolute inset-0 flex items-center justify-center'>
@ -133,7 +128,7 @@ function renderInboxAttachment(object: ObjectProperties) {
case 'audio/mpeg': case 'audio/mpeg':
case 'audio/ogg': case 'audio/ogg':
return ( return (
<div className='min-w-[160px]'> <div className='ml-8 w-[120px]'>
<div className='relative mb-4 mt-2 w-full'> <div className='relative mb-4 mt-2 w-full'>
<audio className='w-full' src={attachment.url} controls/> <audio className='w-full' src={attachment.url} controls/>
</div> </div>
@ -141,9 +136,7 @@ function renderInboxAttachment(object: ObjectProperties) {
); );
default: default:
if (object.image) { if (object.image) {
return <div className='min-h-[80px] min-w-[120px]'> return <img className={imageAttachmentStyles} src={object.image} />;
<img className={`h-[80px] w-[120px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={object.image} />
</div>;
} }
return null; return null;
} }
@ -391,31 +384,25 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
return ( return (
<> <>
{object && ( {object && (
<div className='group/article relative -mx-4 -my-px flex min-w-0 cursor-pointer justify-between rounded-md p-4 hover:bg-grey-75' data-layout='inbox' data-object-id={object.id} onClick={onClick}> <div className='group/article relative -mx-4 -my-px flex min-h-[112px] min-w-0 cursor-pointer items-center justify-between rounded-md p-4 hover:bg-grey-75' data-layout='inbox' data-object-id={object.id} onClick={onClick}>
<div className='flex w-full min-w-0 flex-col items-start justify-between gap-1 pr-4'> <div className='flex min-h-[73px] w-full min-w-0 flex-col items-start justify-start'>
<div className='z-10 flex w-full min-w-0 items-start gap-2 group-hover/article:border-transparent'> <div className='z-10 mb-1 flex w-full min-w-0 items-center gap-1.5 text-base text-grey-700 group-hover/article:border-transparent'>
<APAvatar author={author} size='xs'/> <APAvatar author={author} size='2xs'/>
<span className='min-w-0 truncate break-all font-semibold' data-test-activity-heading>{author.name}</span> <span className='min-w-0 truncate break-all font-medium text-grey-900' data-test-activity-heading>{author.name}</span>
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span> <span className='min-w-0 truncate'>{getUsername(author)}</span>
{/* <div className='flex gap-2'> <span className='shrink-0 whitespace-nowrap before:mr-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
<span className='truncate min-w-0 break-all font-semibold' data-test-activity-heading>{author.name}</span>
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
</div> */}
<span className='shrink-0 whitespace-nowrap text-grey-700 before:mr-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
</div> </div>
<Heading className='line-clamp-1 font-semibold leading-normal' level={5} data-test-activity-heading> <Heading className='mb-1 line-clamp-1 w-full max-w-[600px] text-[1.6rem] font-semibold leading-snug' level={5} data-test-activity-heading>
{object.name ? object.name : ( {object.name ? object.name : (
<span dangerouslySetInnerHTML={{ <span dangerouslySetInnerHTML={{
__html: object.content.length > 30 __html: stripHtml(object.content)
? stripHtml(object.content).substring(0, 50) + '...'
: stripHtml(object.content)
}}></span> }}></span>
)} )}
</Heading> </Heading>
<div dangerouslySetInnerHTML={({__html: stripHtml(object.content)})} className='ap-note-content w-full truncate text-[1.5rem] text-grey-700'></div> <div dangerouslySetInnerHTML={({__html: stripHtml(object.content)})} className='ap-note-content w-full max-w-[600px] truncate text-base leading-normal text-grey-700'></div>
</div> </div>
{renderInboxAttachment(object)} {renderInboxAttachment(object)}
<div className='invisible absolute right-2 top-[9px] z-[49] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'> <div className='invisible absolute right-4 top-[9px] z-[49] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'>
<FeedItemStats <FeedItemStats
commentCount={commentCount} commentCount={commentCount}
layout={layout} layout={layout}

View file

@ -3,7 +3,7 @@ import clsx from 'clsx';
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub'; import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
import {Icon} from '@tryghost/admin-x-design-system'; import {Icon} from '@tryghost/admin-x-design-system';
type AvatarSize = 'xs' | 'sm' | 'lg'; type AvatarSize = '2xs' | 'xs' | 'sm' | 'lg';
export type AvatarBadge = 'user-fill' | 'heart-fill' | 'comment-fill' | undefined; export type AvatarBadge = 'user-fill' | 'heart-fill' | 'comment-fill' | undefined;
interface APAvatarProps { interface APAvatarProps {
@ -37,6 +37,11 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
} }
switch (size) { switch (size) {
case '2xs':
iconSize = 10;
containerClass = clsx('h-4 w-4 rounded ', containerClass);
imageClass = 'z-10 rounded w-4 h-4 object-cover';
break;
case 'xs': case 'xs':
iconSize = 12; iconSize = 12;
containerClass = clsx('h-5 w-5 rounded ', containerClass); containerClass = clsx('h-5 w-5 rounded ', containerClass);