mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Updated ActivityPub design (#21327)
ref https://linear.app/ghost/issue/AP-476/remove-static-buttons-from-notifications-and-resolve-css-issues, https://linear.app/ghost/issue/AP-449/remove-follow-button-and-component, https://linear.app/ghost/issue/AP-448/add-loading-state-for-home-tab, https://linear.app/ghost/issue/AP-446/update-top-nav-bar-design - Added the Suggestions sidebar - Added real data to `Your profile` tab - Switched navigation in top-bar to text-based - Added loading states to Home and Activity tabs - Fixed overflow and z-index CSS issues - Removed `Unfollow` modal since now have a more user-friendly way to follow users - Changed link color to blue
This commit is contained in:
parent
4277c2a9d5
commit
7c32527159
16 changed files with 309 additions and 241 deletions
|
@ -6,12 +6,12 @@ import {LoadingIndicator, NoValueLabel} from '@tryghost/admin-x-design-system';
|
|||
import APAvatar, {AvatarBadge} from './global/APAvatar';
|
||||
import ActivityItem, {type Activity} from './activities/ActivityItem';
|
||||
import ArticleModal from './feed/ArticleModal';
|
||||
import FollowButton from './global/FollowButton';
|
||||
// import FollowButton from './global/FollowButton';
|
||||
import MainNavigation from './navigation/MainNavigation';
|
||||
|
||||
import getUsername from '../utils/get-username';
|
||||
import {useActivitiesForUser} from '../hooks/useActivityPubQueries';
|
||||
import {useFollowersForUser} from '../MainContent';
|
||||
// import {useFollowersForUser} from '../MainContent';
|
||||
|
||||
interface ActivitiesProps {}
|
||||
|
||||
|
@ -91,7 +91,8 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
data,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
isFetchingNextPage,
|
||||
isLoading
|
||||
} = useActivitiesForUser({
|
||||
handle: user,
|
||||
includeOwn: true,
|
||||
|
@ -129,66 +130,75 @@ const Activities: React.FC<ActivitiesProps> = ({}) => {
|
|||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
// Retrieve followers for the user
|
||||
const {data: followers = []} = useFollowersForUser(user);
|
||||
// const {data: followers = []} = useFollowersForUser(user);
|
||||
|
||||
const isFollower = (id: string): boolean => {
|
||||
return followers.includes(id);
|
||||
};
|
||||
// const isFollower = (id: string): boolean => {
|
||||
// return followers.includes(id);
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainNavigation title='Activities' />
|
||||
<div className='z-0 flex w-full flex-col items-center'>
|
||||
{activities.length === 0 && (
|
||||
<div className='mt-8'>
|
||||
<NoValueLabel icon='bell'>
|
||||
When other Fediverse users interact with you, you'll see it here.
|
||||
</NoValueLabel>
|
||||
</div>
|
||||
)}
|
||||
{activities.length > 0 && (
|
||||
<>
|
||||
<div className='mt-8 flex w-full max-w-[560px] flex-col'>
|
||||
{activities?.map(activity => (
|
||||
<ActivityItem
|
||||
key={activity.id}
|
||||
url={getActivityUrl(activity) || getActorUrl(activity)}
|
||||
onClick={
|
||||
activity.type === ACTVITY_TYPE.CREATE ? () => {
|
||||
NiceModal.show(ArticleModal, {
|
||||
object: activity.object,
|
||||
actor: activity.actor,
|
||||
comments: activity.object.replies
|
||||
});
|
||||
} : undefined
|
||||
}
|
||||
>
|
||||
<APAvatar author={activity.actor} badge={getActivityBadge(activity)} />
|
||||
<div className='pt-[2px]'>
|
||||
<div className='text-grey-600'>
|
||||
<span className='mr-1 font-bold text-black'>{activity.actor.name}</span>
|
||||
{getUsername(activity.actor)}
|
||||
</div>
|
||||
<div className=''>{getActivityDescription(activity)}</div>
|
||||
{getExtendedDescription(activity)}
|
||||
</div>
|
||||
<FollowButton
|
||||
className='ml-auto'
|
||||
following={isFollower(activity.actor.id)}
|
||||
handle={getUsername(activity.actor)}
|
||||
type='link'
|
||||
/>
|
||||
</ActivityItem>
|
||||
))}
|
||||
{
|
||||
isLoading && (<div className='mt-8 flex flex-col items-center justify-center space-y-4 text-center'>
|
||||
<LoadingIndicator size='lg' />
|
||||
</div>)
|
||||
}
|
||||
{
|
||||
isLoading === false && activities.length === 0 && (
|
||||
<div className='mt-8'>
|
||||
<NoValueLabel icon='bell'>
|
||||
When other Fediverse users interact with you, you'll see it here.
|
||||
</NoValueLabel>
|
||||
</div>
|
||||
<div ref={loadMoreRef} className='h-1'></div>
|
||||
{isFetchingNextPage && (
|
||||
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
|
||||
<LoadingIndicator size='md' />
|
||||
)
|
||||
}
|
||||
{
|
||||
(isLoading === false && activities.length > 0) && (
|
||||
<>
|
||||
<div className='mt-8 flex w-full max-w-[560px] flex-col'>
|
||||
{activities?.map(activity => (
|
||||
<ActivityItem
|
||||
key={activity.id}
|
||||
url={getActivityUrl(activity) || getActorUrl(activity)}
|
||||
onClick={
|
||||
activity.type === ACTVITY_TYPE.CREATE ? () => {
|
||||
NiceModal.show(ArticleModal, {
|
||||
object: activity.object,
|
||||
actor: activity.actor,
|
||||
comments: activity.object.replies
|
||||
});
|
||||
} : undefined
|
||||
}
|
||||
>
|
||||
<APAvatar author={activity.actor} badge={getActivityBadge(activity)} />
|
||||
<div className='min-w-0'>
|
||||
<div className='truncate text-grey-600'>
|
||||
<span className='mr-1 font-bold text-black'>{activity.actor.name}</span>
|
||||
{getUsername(activity.actor)}
|
||||
</div>
|
||||
<div className=''>{getActivityDescription(activity)}</div>
|
||||
{getExtendedDescription(activity)}
|
||||
</div>
|
||||
{/* <FollowButton
|
||||
className='ml-auto'
|
||||
following={isFollower(activity.actor.id)}
|
||||
handle={getUsername(activity.actor)}
|
||||
type='link'
|
||||
/> */}
|
||||
</ActivityItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div ref={loadMoreRef} className='h-1'></div>
|
||||
{isFetchingNextPage && (
|
||||
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
|
||||
<LoadingIndicator size='md' />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import APAvatar from './global/APAvatar';
|
||||
import ActivityItem, {type Activity} from './activities/ActivityItem';
|
||||
import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png';
|
||||
import ArticleModal from './feed/ArticleModal';
|
||||
import FeedItem from './feed/FeedItem';
|
||||
import MainNavigation from './navigation/MainNavigation';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {type Activity} from './activities/ActivityItem';
|
||||
import getUsername from '../utils/get-username';
|
||||
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
import {Button, Heading, LoadingIndicator} from '@tryghost/admin-x-design-system';
|
||||
import {useActivitiesForUser} from '../hooks/useActivityPubQueries';
|
||||
import {useActivitiesForUser, useSuggestedProfiles} from '../hooks/useActivityPubQueries';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
interface InboxProps {}
|
||||
|
||||
|
@ -20,7 +23,8 @@ const Inbox: React.FC<InboxProps> = ({}) => {
|
|||
data,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage
|
||||
isFetchingNextPage,
|
||||
isLoading
|
||||
} = useActivitiesForUser({
|
||||
handle: 'index',
|
||||
includeReplies: true,
|
||||
|
@ -29,6 +33,11 @@ const Inbox: React.FC<InboxProps> = ({}) => {
|
|||
}
|
||||
});
|
||||
|
||||
const {updateRoute} = useRouting();
|
||||
|
||||
const {suggestedProfilesQuery} = useSuggestedProfiles('index', ['@quillmatiq@mastodon.social', '@miaq@flipboard.social', '@mallory@techpolicy.social']);
|
||||
const {data: suggested = [], isLoading: isLoadingSuggested} = suggestedProfilesQuery;
|
||||
|
||||
const activities = (data?.pages.flatMap(page => page.data) ?? []);
|
||||
|
||||
const handleViewContent = (object: ObjectProperties, actor: ActorProperties, comments: Activity[], focusReply = false) => {
|
||||
|
@ -96,45 +105,88 @@ const Inbox: React.FC<InboxProps> = ({}) => {
|
|||
<>
|
||||
<MainNavigation page='home' title="Home" onLayoutChange={handleLayoutChange} />
|
||||
<div className='z-0 my-5 flex w-full flex-col'>
|
||||
<div className='w-full'>
|
||||
{activities.length > 0 ? (
|
||||
<div className='w-full px-8'>
|
||||
{isLoading ? (
|
||||
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
|
||||
<LoadingIndicator size='lg' />
|
||||
</div>
|
||||
) : activities.length > 0 ? (
|
||||
<>
|
||||
<ul className='mx-auto flex max-w-[640px] flex-col'>
|
||||
{activities.map((activity, index) => (
|
||||
<li
|
||||
key={activity.id}
|
||||
data-test-view-article
|
||||
onClick={() => handleViewContent(
|
||||
activity.object,
|
||||
getContentAuthor(activity),
|
||||
activity.object.replies
|
||||
<div className={`mx-auto flex items-start ${layout === 'inbox' ? 'max-w-6xl gap-14' : 'gap-8'}`}>
|
||||
<div className='flex w-full min-w-0 items-start'>
|
||||
<ul className={`mx-auto flex ${layout === 'inbox' ? 'max-w-full' : 'max-w-[500px]'} flex-col`}>
|
||||
{activities.map((activity, index) => (
|
||||
<li
|
||||
key={activity.id}
|
||||
data-test-view-article
|
||||
onClick={() => handleViewContent(
|
||||
activity.object,
|
||||
getContentAuthor(activity),
|
||||
activity.object.replies
|
||||
)}
|
||||
>
|
||||
<FeedItem
|
||||
actor={activity.actor}
|
||||
comments={activity.object.replies}
|
||||
layout={layout}
|
||||
object={activity.object}
|
||||
type={activity.type}
|
||||
onCommentClick={() => handleViewContent(
|
||||
activity.object,
|
||||
getContentAuthor(activity),
|
||||
activity.object.replies,
|
||||
true
|
||||
)}
|
||||
/>
|
||||
{index < activities.length - 1 && (
|
||||
<div className="h-px w-full bg-grey-200"></div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<div ref={loadMoreRef} className='h-1'></div>
|
||||
{isFetchingNextPage && (
|
||||
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
|
||||
<LoadingIndicator size='md' />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<FeedItem
|
||||
actor={activity.actor}
|
||||
comments={activity.object.replies}
|
||||
layout={layout}
|
||||
object={activity.object}
|
||||
type={activity.type}
|
||||
onCommentClick={() => handleViewContent(
|
||||
activity.object,
|
||||
getContentAuthor(activity),
|
||||
activity.object.replies,
|
||||
true
|
||||
)}
|
||||
/>
|
||||
{index < activities.length - 1 && (
|
||||
<div className="h-px w-full bg-grey-200"></div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div ref={loadMoreRef} className='h-1'></div>
|
||||
{isFetchingNextPage && (
|
||||
<div className='flex flex-col items-center justify-center space-y-4 text-center'>
|
||||
<LoadingIndicator size='md' />
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className={`sticky top-[135px] ml-auto w-full max-w-[300px] max-lg:hidden ${layout === 'inbox' ? '' : ' xxl:fixed xxl:right-[40px]'}`}>
|
||||
<h2 className='mb-2 text-lg font-semibold'>You might also like...</h2>
|
||||
{isLoadingSuggested ? (
|
||||
<LoadingIndicator size="sm" />
|
||||
) : (
|
||||
<ul className='grow'>
|
||||
{suggested.map((profile) => {
|
||||
const actor = profile.actor;
|
||||
// const isFollowing = profile.isFollowing;
|
||||
return (
|
||||
<li key={actor.id}>
|
||||
<ActivityItem url={actor.url}>
|
||||
<APAvatar author={actor} />
|
||||
<div>
|
||||
<div className='text-grey-600'>
|
||||
<span className='mr-1 truncate font-bold text-black'>{actor.name || actor.preferredUsername || 'Unknown'}</span>
|
||||
<div className='truncate text-sm'>{getUsername(actor)}</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
<Button className='mt-4' color='grey' fullWidth={true} label='Explore' onClick={() => updateRoute('search')} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className='flex items-center justify-center text-center'>
|
||||
|
@ -148,7 +200,7 @@ const Inbox: React.FC<InboxProps> = ({}) => {
|
|||
Welcome to ActivityPub
|
||||
</Heading>
|
||||
<p className='text-pretty text-grey-800'>
|
||||
We’re so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost.
|
||||
We're so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost.
|
||||
</p>
|
||||
<p className='text-pretty text-grey-800'>
|
||||
You can see all of the users on the right—find your favorite ones and give them a follow.
|
||||
|
|
|
@ -2,10 +2,18 @@ import APAvatar from './global/APAvatar';
|
|||
import ActivityItem from './activities/ActivityItem';
|
||||
import FeedItem from './feed/FeedItem';
|
||||
import MainNavigation from './navigation/MainNavigation';
|
||||
import React, {useState} from 'react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import getUsername from '../utils/get-username';
|
||||
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
import {Button, Heading, List, NoValueLabel, Tab, TabView} from '@tryghost/admin-x-design-system';
|
||||
import {useFollowersCountForUser, useFollowersForUser, useFollowingCountForUser, useFollowingForUser, useLikedForUser} from '../hooks/useActivityPubQueries';
|
||||
import {
|
||||
useFollowersCountForUser,
|
||||
useFollowersForUser,
|
||||
useFollowingCountForUser,
|
||||
useFollowingForUser,
|
||||
useLikedForUser,
|
||||
useUserDataForUser
|
||||
} from '../hooks/useActivityPubQueries';
|
||||
|
||||
interface ProfileProps {}
|
||||
|
||||
|
@ -15,6 +23,8 @@ const Profile: React.FC<ProfileProps> = ({}) => {
|
|||
const {data: following = []} = useFollowingForUser('index');
|
||||
const {data: followers = []} = useFollowersForUser('index');
|
||||
const {data: liked = []} = useLikedForUser('index');
|
||||
// Replace 'index' with the actual handle of the user
|
||||
const {data: userProfile} = useUserDataForUser('index') as {data: ActorProperties | null};
|
||||
|
||||
type ProfileTab = 'posts' | 'likes' | 'following' | 'followers';
|
||||
|
||||
|
@ -131,25 +141,75 @@ const Profile: React.FC<ProfileProps> = ({}) => {
|
|||
}
|
||||
].filter(Boolean) as Tab<ProfileTab>[];
|
||||
|
||||
const attachments = (userProfile?.attachment || []);
|
||||
|
||||
const [isExpanded, setisExpanded] = useState(false);
|
||||
|
||||
const toggleExpand = () => {
|
||||
setisExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
setIsOverflowing(contentRef.current.scrollHeight > 160); // Compare content height to max height
|
||||
}
|
||||
}, [isExpanded]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainNavigation title='Profile' />
|
||||
<div className='z-0 flex w-full flex-col items-center'>
|
||||
<div className='mx-auto mt-8 w-full max-w-[560px]' id='ap-sidebar'>
|
||||
<div className='h-[200px] w-full rounded-lg bg-gradient-to-tr from-grey-200 to-grey-100'>
|
||||
</div>
|
||||
<div className='-mt-8 px-4'>
|
||||
<div className='inline-flex rounded-lg border-4 border-white'>
|
||||
<APAvatar size='lg' />
|
||||
<div className='z-0 mx-auto mt-8 flex w-full max-w-[580px] flex-col items-center pb-16'>
|
||||
<div className='mx-auto w-full'>
|
||||
{userProfile?.image && (<div className='h-[200px] w-full overflow-hidden rounded-lg bg-gradient-to-tr from-grey-200 to-grey-100'>
|
||||
<img
|
||||
alt={userProfile?.name}
|
||||
className='h-full w-full object-cover'
|
||||
src={userProfile?.image.url}
|
||||
/>
|
||||
</div>)}
|
||||
<div className={`${userProfile?.image && '-mt-12'} px-4`}>
|
||||
<div className='flex items-end justify-between'>
|
||||
<div className='rounded-xl outline outline-4 outline-white'>
|
||||
<APAvatar
|
||||
author={userProfile as ActorProperties}
|
||||
size='lg'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Heading className='mt-4' level={3}>Building ActivityPub</Heading>
|
||||
<span className='mt-1 text-[1.5rem] text-grey-800'>@index@activitypub.ghost.org</span>
|
||||
<p className='mt-3 text-[1.5rem]'>Ghost is federating over ActivityPub to become part of the world's largest publishing network</p>
|
||||
<span className='mt-3 line-clamp-1 flex'>
|
||||
<span className={`mr-1 after:content-[":"]`}>Website</span>
|
||||
<a className='truncate text-[1.5rem] underline' href='https://activitypub.ghost.org'>activitypub.ghost.org</a>
|
||||
<Heading className='mt-4' level={3}>{userProfile?.name}</Heading>
|
||||
<span className='mt-1 text-[1.5rem] text-grey-800'>
|
||||
<span>{userProfile && getUsername(userProfile)}</span>
|
||||
</span>
|
||||
<TabView<'posts' | 'likes' | 'following' | 'followers'> containerClassName='mt-6' selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
|
||||
{(userProfile?.summary || attachments.length > 0) && (<div ref={contentRef} className={`ap-profile-content transition-max-height relative text-[1.5rem] duration-300 ease-in-out [&>p]:mb-3 ${isExpanded ? 'max-h-none pb-7' : 'max-h-[160px] overflow-hidden'} relative`}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{__html: userProfile?.summary ?? ''}}
|
||||
className='ap-profile-content mt-3 text-[1.5rem] [&>p]:mb-3'
|
||||
/>
|
||||
{attachments.map(attachment => (
|
||||
<span className='mt-3 line-clamp-1 flex flex-col text-[1.5rem]'>
|
||||
<span className={`text-xs font-semibold`}>{attachment.name}</span>
|
||||
<span dangerouslySetInnerHTML={{__html: attachment.value}} className='ap-profile-content truncate'/>
|
||||
</span>
|
||||
))}
|
||||
{!isExpanded && isOverflowing && (
|
||||
<div className='absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-white via-white/90 via-60% to-transparent' />
|
||||
)}
|
||||
{isOverflowing && <Button
|
||||
className='absolute bottom-0 text-pink'
|
||||
label={isExpanded ? 'Show less' : 'Show all'}
|
||||
link={true}
|
||||
onClick={toggleExpand}
|
||||
/>}
|
||||
</div>)}
|
||||
<TabView<ProfileTab>
|
||||
containerClassName='mt-6'
|
||||
selectedTab={selectedTab}
|
||||
tabs={tabs}
|
||||
onTabChange={setSelectedTab}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -86,7 +86,7 @@ const Search: React.FC<SearchProps> = ({}) => {
|
|||
|
||||
const results = data?.profiles || [];
|
||||
const showLoading = (isFetching || isQuerying) && !isFetched;
|
||||
const showNoResults = isFetched && results.length === 0;
|
||||
const showNoResults = isFetched && results.length === 0 && (query.length > 0);
|
||||
const showSuggested = query === '' || (isFetched && results.length === 0);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -26,7 +26,7 @@ const ActivityItem: React.FC<ActivityItemProps> = ({children, url = null, onClic
|
|||
onClick();
|
||||
}
|
||||
}}>
|
||||
<div className='flex w-full gap-4 border-b border-grey-100 px-2 py-4'>
|
||||
<div className='flex w-full gap-3 border-b border-grey-100 px-2 py-4'>
|
||||
{childrenArray[0]}
|
||||
{childrenArray[1]}
|
||||
{childrenArray[2]}
|
||||
|
|
|
@ -146,7 +146,6 @@ const ArticleModal: React.FC<ArticleModalProps> = ({object, actor, comments, foc
|
|||
</div>
|
||||
)}
|
||||
<div className='col-[2/3] flex grow items-center justify-center px-8 text-center'>
|
||||
{/* <span className='text-lg font-semibold text-grey-900'>{object.type}</span> */}
|
||||
</div>
|
||||
<div className='col-[3/4] flex items-center justify-end space-x-6 px-8'>
|
||||
<Button icon='angle-brackets' size='md' unstyled onClick={toggleModalSize}/>
|
||||
|
|
|
@ -6,6 +6,7 @@ import APAvatar from '../global/APAvatar';
|
|||
|
||||
import getRelativeTimestamp from '../../utils/get-relative-timestamp';
|
||||
import getUsername from '../../utils/get-username';
|
||||
import stripHtml from '../../utils/strip-html';
|
||||
import {type Activity} from '../activities/ActivityItem';
|
||||
import {useLikeMutationForUser, useUnlikeMutationForUser} from '../../hooks/useActivityPubQueries';
|
||||
|
||||
|
@ -140,8 +141,8 @@ function renderInboxAttachment(object: ObjectProperties) {
|
|||
);
|
||||
default:
|
||||
if (object.image) {
|
||||
return <div className='min-w-[160px]'>
|
||||
<img className={`h-[100px] w-[160px] rounded-md object-cover outline outline-1 -outline-offset-1 outline-black/10`} src={object.image} />
|
||||
return <div className='min-h-[80px]'>
|
||||
<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;
|
||||
|
@ -169,7 +170,8 @@ const FeedItemStats: React.FC<{
|
|||
const likeMutation = useLikeMutationForUser('index');
|
||||
const unlikeMutation = useUnlikeMutationForUser('index');
|
||||
|
||||
const handleLikeClick = async () => {
|
||||
const handleLikeClick = async (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsClicked(true);
|
||||
if (!isLiked) {
|
||||
likeMutation.mutate(object.id);
|
||||
|
@ -194,7 +196,9 @@ const FeedItemStats: React.FC<{
|
|||
unstyled={true}
|
||||
onClick={(e?: React.MouseEvent<HTMLElement>) => {
|
||||
e?.stopPropagation();
|
||||
handleLikeClick();
|
||||
if (e) {
|
||||
handleLikeClick(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isLiked && (layout !== 'inbox') && <span className={`text-grey-900`}>{new Intl.NumberFormat().format(likeCount)}</span>}
|
||||
|
@ -250,7 +254,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
if (object?.url) { // Check if url is defined
|
||||
if (object?.url) {
|
||||
await navigator.clipboard.writeText(object.url);
|
||||
setIsCopied(true);
|
||||
showToast({
|
||||
|
@ -284,7 +288,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
|
||||
const UserMenuTrigger = (
|
||||
<Button
|
||||
className={`relative z-10 ml-auto flex h-5 w-5 items-center justify-center self-start hover:opacity-60 ${isCopied ? 'bump' : ''}`}
|
||||
className={`relative z-[9998] ml-auto flex h-5 w-5 items-center justify-center self-start hover:opacity-60 ${isCopied ? 'bump' : ''}`}
|
||||
hideLabel={true}
|
||||
icon='dotdotdot'
|
||||
iconColorClass={`(${layout === 'inbox' ? 'text-grey-900' : 'text-grey-600'}`}
|
||||
|
@ -303,20 +307,20 @@ 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 pb-6`} data-test-activity>
|
||||
<div className={`border-1 -my-1 grid grid-cols-[auto_1fr] grid-rows-[auto_1fr] gap-x-3 gap-y-2 pb-6`} data-test-activity>
|
||||
<APAvatar author={author}/>
|
||||
<div className='flex justify-between'>
|
||||
<div className='relative z-10 flex w-full flex-col overflow-visible text-[1.5rem]'>
|
||||
<div className='flex min-w-0 justify-between'>
|
||||
<div className='relative z-10 flex w-full flex-col overflow-visible text-md'>
|
||||
<div className='flex justify-between'>
|
||||
<div className='flex'>
|
||||
<span className='truncate whitespace-nowrap font-bold' data-test-activity-heading>{author.name}</span>
|
||||
<div className='flex w-full'>
|
||||
<span className='min-w-0 truncate break-all font-semibold' data-test-activity-heading>{author.name}</span>
|
||||
<span className='ml-1 truncate text-grey-700'>{getUsername(author)}</span>
|
||||
</div>
|
||||
{renderTimestamp(object)}
|
||||
<div className='ml-2'>{renderTimestamp(object)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`relative z-10 col-start-2 col-end-3 w-full gap-4`}>
|
||||
<div className={`relative col-start-2 col-end-3 w-full gap-4`}>
|
||||
<div className='flex flex-col'>
|
||||
<div className='mt-[-24px]'>
|
||||
{(object.type === 'Article') && renderFeedAttachment(object, layout)}
|
||||
|
@ -332,7 +336,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
size='md'
|
||||
/>}
|
||||
</div>
|
||||
<div className='space-between mt-5 flex'>
|
||||
<div className='space-between relative z-[30] mt-5 flex'>
|
||||
<FeedItemStats
|
||||
commentCount={comments.length}
|
||||
layout={layout}
|
||||
|
@ -345,9 +349,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
{/* <div className={`absolute -inset-x-3 -inset-y-0 z-0 rounded transition-colors ${(layout === 'feed') ? 'group-hover/article:bg-grey-75' : ''} `}></div> */}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -357,7 +359,7 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
<>
|
||||
{object && (
|
||||
<div>
|
||||
<div className={`group/article relative cursor-pointer`} onClick={onClick}>
|
||||
<div className={`group/article relative`} 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>
|
||||
|
@ -366,14 +368,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
<div className='relative z-10 pt-[3px]'>
|
||||
<APAvatar author={author}/>
|
||||
</div>
|
||||
{/* <div className='border-1 z-10 -mt-1 flex w-full flex-col items-start justify-between border-b border-b-grey-200 pb-4' data-test-activity> */}
|
||||
<div className='relative z-10 flex w-full flex-col overflow-visible text-[1.5rem]'>
|
||||
<div className='flex'>
|
||||
<span className='truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
|
||||
{renderTimestamp(object)}
|
||||
<div className='relative z-10 flex w-full min-w-0 flex-col overflow-visible text-[1.5rem]'>
|
||||
<div className='flex w-full'>
|
||||
<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 className='ml-2'>{renderTimestamp(object)}</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<span className='truncate text-grey-700'>{getUsername(author)}</span>
|
||||
<div className='flex w-full'>
|
||||
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`relative z-10 col-start-1 col-end-3 w-full gap-4`}>
|
||||
|
@ -394,7 +395,6 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
<div className={`absolute -inset-x-3 -inset-y-0 z-0 rounded transition-colors`}></div>
|
||||
</div>
|
||||
|
@ -414,13 +414,13 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
<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 pt-[3px]'>
|
||||
<div className='relative z-10 min-w-0 pt-[3px]'>
|
||||
<APAvatar author={author}/>
|
||||
</div>
|
||||
<div className='relative z-10 flex w-full flex-col overflow-visible text-[1.5rem]'>
|
||||
<div className='relative z-10 flex w-full min-w-0 flex-col overflow-visible text-[1.5rem]'>
|
||||
<div className='flex'>
|
||||
<span className='truncate whitespace-nowrap font-bold after:mx-1 after:font-normal after:text-grey-700 after:content-["·"]' data-test-activity-heading>{author.name}</span>
|
||||
{renderTimestamp(object)}
|
||||
<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 className='ml-2'>{renderTimestamp(object)}</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<span className='truncate text-grey-700'>{getUsername(author)}</span>
|
||||
|
@ -455,23 +455,31 @@ const FeedItem: React.FC<FeedItemProps> = ({actor, object, layout, type, comment
|
|||
return (
|
||||
<>
|
||||
{object && (
|
||||
<div className='group/article relative -mx-4 -mt-px flex cursor-pointer justify-between rounded-md p-4 hover:bg-grey-75' onClick={onClick}>
|
||||
<div className='flex w-full flex-col items-start justify-between gap-1 pr-4'>
|
||||
<div className='z-10 flex items-start justify-between gap-2 group-hover/article:border-transparent'>
|
||||
<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' onClick={onClick}>
|
||||
<div className='flex w-full min-w-0 flex-col items-start justify-between gap-1 pr-4'>
|
||||
<div className='z-10 flex w-full min-w-0 items-start gap-2 group-hover/article:border-transparent'>
|
||||
<APAvatar author={author} size='xs'/>
|
||||
<div className='z-10 w-full text-sm'>
|
||||
<div>
|
||||
<span className='truncate whitespace-nowrap font-semibold' data-test-activity-heading>{author.name}</span>
|
||||
<span className='truncate text-grey-700'> {getUsername(author)}</span>
|
||||
<span className='whitespace-nowrap text-grey-700 before:mx-1 before:content-["·"]' title={`${timestamp}`}>{getRelativeTimestamp(date)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className='min-w-0 truncate break-all font-semibold' data-test-activity-heading>{author.name}</span>
|
||||
<span className='min-w-0 truncate text-grey-700'>{getUsername(author)}</span>
|
||||
{/* <div className='flex gap-2'>
|
||||
<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>
|
||||
<Heading className='line-clamp-1 font-semibold leading-normal' level={5} data-test-activity-heading>{object.name ? object.name : <span dangerouslySetInnerHTML={({__html: object.content})}></span>}</Heading>
|
||||
<div dangerouslySetInnerHTML={({__html: object.content})} className='ap-note-content line-clamp-1 text-pretty text-[1.5rem] text-grey-700'></div>
|
||||
<Heading className='line-clamp-1 font-semibold leading-normal' level={5} data-test-activity-heading>
|
||||
{object.name ? object.name : (
|
||||
<span dangerouslySetInnerHTML={{
|
||||
__html: object.content.length > 30
|
||||
? stripHtml(object.content).substring(0, 50) + '...'
|
||||
: stripHtml(object.content)
|
||||
}}></span>
|
||||
)}
|
||||
</Heading>
|
||||
<div dangerouslySetInnerHTML={({__html: stripHtml(object.content)})} className='ap-note-content w-full truncate text-[1.5rem] text-grey-700'></div>
|
||||
</div>
|
||||
{renderInboxAttachment(object)}
|
||||
<div className='invisible absolute right-2 top-[9px] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'>
|
||||
<div className='invisible absolute right-2 top-[9px] z-[9998] flex flex-col gap-2 rounded-lg bg-white p-2 shadow-md-heavy group-hover/article:visible'>
|
||||
<FeedItemStats
|
||||
commentCount={comments.length}
|
||||
layout={layout}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {useState} from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
import {Icon} from '@tryghost/admin-x-design-system';
|
||||
|
||||
|
@ -15,10 +15,14 @@ const APAvatar: React.FC<APAvatarProps> = ({author, size, badge}) => {
|
|||
let iconSize = 18;
|
||||
let containerClass = '';
|
||||
let imageClass = 'z-10 rounded w-10 h-10 object-cover';
|
||||
const badgeClass = `w-6 h-6 z-20 rounded-full absolute -bottom-2 -right-2 border-2 border-white content-box flex items-center justify-center `;
|
||||
const badgeClass = `w-6 h-6 z-20 rounded-full absolute -bottom-2 -right-[0.6rem] border-2 border-white content-box flex items-center justify-center `;
|
||||
let badgeColor = '';
|
||||
const [iconUrl, setIconUrl] = useState(author?.icon?.url);
|
||||
|
||||
useEffect(() => {
|
||||
setIconUrl(author?.icon?.url);
|
||||
}, [author?.icon?.url]);
|
||||
|
||||
switch (badge) {
|
||||
case 'user-fill':
|
||||
badgeColor = ' bg-blue-500';
|
||||
|
|
|
@ -8,7 +8,6 @@ import {Activity} from '../activities/ActivityItem';
|
|||
import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub';
|
||||
import {Button, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {useReplyMutationForUser, useUserDataForUser} from '../../hooks/useActivityPubQueries';
|
||||
// import {useFocusContext} from '@tryghost/admin-x-design-system/types/providers/DesignSystemProvider';
|
||||
|
||||
export interface APTextAreaProps extends HTMLProps<HTMLTextAreaElement> {
|
||||
title?: string;
|
||||
|
@ -35,9 +34,6 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
|
|||
object,
|
||||
focused,
|
||||
onNewReply,
|
||||
// onChange,
|
||||
// onFocus,
|
||||
// onBlur,
|
||||
...props
|
||||
}) => {
|
||||
const id = useId();
|
||||
|
@ -131,8 +127,8 @@ const APReplyBox: React.FC<APTextAreaProps> = ({
|
|||
{hint}
|
||||
</div>
|
||||
</FormPrimitive.Root>
|
||||
<div className='absolute bottom-[6px] right-[9px] flex space-x-4 transition-[opacity] duration-150'>
|
||||
<Button color='black' disabled={buttonDisabled} id='post' label='Post' loading={replyMutation.isLoading} size='sm' onMouseDown={handleClick} />
|
||||
<div className='absolute bottom-[3px] right-[9px] 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>
|
||||
</div>
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import {useState} from 'react';
|
||||
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import {Modal, TextField, showToast} from '@tryghost/admin-x-design-system';
|
||||
import {useRouting} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
import {useFollow} from '../../hooks/useActivityPubQueries';
|
||||
|
||||
const FollowSite = NiceModal.create(() => {
|
||||
const {updateRoute} = useRouting();
|
||||
const modal = NiceModal.useModal();
|
||||
const [profileName, setProfileName] = useState('');
|
||||
const [errorMessage, setError] = useState(null);
|
||||
|
||||
async function onSuccess() {
|
||||
showToast({
|
||||
message: 'Site followed',
|
||||
type: 'success'
|
||||
});
|
||||
|
||||
modal.remove();
|
||||
updateRoute('');
|
||||
}
|
||||
async function onError() {
|
||||
setError(errorMessage);
|
||||
}
|
||||
const mutation = useFollow('index', onSuccess, onError);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
afterClose={() => {
|
||||
mutation.reset();
|
||||
updateRoute('');
|
||||
}}
|
||||
cancelLabel='Cancel'
|
||||
okLabel='Follow'
|
||||
size='sm'
|
||||
title='Follow a Ghost site'
|
||||
onOk={() => mutation.mutate(profileName)}
|
||||
>
|
||||
<div className='mt-3 flex flex-col gap-4'>
|
||||
<TextField
|
||||
autoFocus={true}
|
||||
error={Boolean(errorMessage)}
|
||||
hint={errorMessage}
|
||||
placeholder='@username@hostname'
|
||||
title='Profile name'
|
||||
value={profileName}
|
||||
data-test-new-follower
|
||||
onChange={e => setProfileName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default FollowSite;
|
|
@ -1,10 +1,9 @@
|
|||
import FollowSite from './inbox/FollowSiteModal';
|
||||
import ViewFollowers from './profile/ViewFollowersModal';
|
||||
import ViewFollowing from './profile/ViewFollowingModal';
|
||||
import {ModalComponent} from '@tryghost/admin-x-framework/routing';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const modals = {FollowSite, ViewFollowing, ViewFollowers} satisfies {[key: string]: ModalComponent<any>};
|
||||
const modals = {ViewFollowing, ViewFollowers} satisfies {[key: string]: ModalComponent<any>};
|
||||
|
||||
export default modals;
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ interface MainNavigationProps {
|
|||
}
|
||||
|
||||
const MainNavigation: React.FC<MainNavigationProps> = ({
|
||||
title = 'Home',
|
||||
page = '',
|
||||
onLayoutChange
|
||||
}) => {
|
||||
|
@ -26,16 +25,11 @@ const MainNavigation: React.FC<MainNavigationProps> = ({
|
|||
|
||||
return (
|
||||
<MainHeader>
|
||||
<div className='col-[1/2] px-8'>
|
||||
<h2 className='mt-1 text-xl font-bold'>
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
<div className='col-[2/3] flex items-center justify-center gap-9'>
|
||||
<Button icon='home' iconColorClass={mainRoute === '' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('')} />
|
||||
<Button icon='magnifying-glass' iconColorClass={mainRoute === 'search' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('search')} />
|
||||
<Button icon='bell' iconColorClass={mainRoute === 'activity' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('activity')} />
|
||||
<Button icon='user' iconColorClass={mainRoute === 'profile' ? 'text-black' : 'text-grey-500'} iconSize={18} unstyled onClick={() => updateRoute('profile')} />
|
||||
<div className='col-[1/2] flex gap-8 px-8'>
|
||||
<Button className={` ${mainRoute === '' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Inbox' unstyled onClick={() => updateRoute('')} />
|
||||
<Button className={` ${mainRoute === 'activity' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Notifications' unstyled onClick={() => updateRoute('activity')} />
|
||||
<Button className={` ${mainRoute === 'search' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Search' unstyled onClick={() => updateRoute('search')} />
|
||||
<Button className={` ${mainRoute === 'profile' ? 'font-bold text-grey-975' : 'text-grey-700 hover:text-grey-800'}`} label='Profile' unstyled onClick={() => updateRoute('profile')} />
|
||||
</div>
|
||||
<div className='col-[3/4] flex items-center justify-end gap-2 px-8'>
|
||||
{page === 'home' &&
|
||||
|
@ -52,9 +46,6 @@ const MainNavigation: React.FC<MainNavigationProps> = ({
|
|||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
<Button color='black' icon='add' label="Follow" onClick={() => {
|
||||
updateRoute('follow-site');
|
||||
}} />
|
||||
</div>
|
||||
</MainHeader>
|
||||
);
|
||||
|
|
|
@ -25,12 +25,14 @@ animation: bump 0.3s ease-in-out;
|
|||
}
|
||||
|
||||
.ap-note-content a, .ap-profile-content a {
|
||||
color: rgb(236 72 153) !important;
|
||||
/* color: rgb(236 72 153) !important; */
|
||||
color: rgb(37 99 235) !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ap-note-content a:hover, .ap-profile-content a:hover {
|
||||
color: rgb(190, 25, 99) !important;
|
||||
/* color: rgb(190, 25, 99) !important; */
|
||||
color: rgb(29 78 216) !important;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
|
|
3
apps/admin-x-activitypub/src/utils/strip-html.ts
Normal file
3
apps/admin-x-activitypub/src/utils/strip-html.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function stripHtml(html: string): string {
|
||||
return html.replace(/<[^>]*>/g, '');
|
||||
}
|
|
@ -27,11 +27,11 @@ const Popover: React.FC<PopoverProps> = ({
|
|||
return (
|
||||
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
|
||||
<PopoverPrimitive.Anchor asChild>
|
||||
<PopoverPrimitive.Trigger asChild>
|
||||
<PopoverPrimitive.Trigger asChild onClick={e => e.stopPropagation()}>
|
||||
{trigger}
|
||||
</PopoverPrimitive.Trigger>
|
||||
</PopoverPrimitive.Anchor>
|
||||
<PopoverPrimitive.Content align={position} className="z-50 mt-2 origin-top-right rounded bg-white shadow-md ring-1 ring-[rgba(0,0,0,0.01)] focus:outline-none dark:bg-grey-900 dark:text-white" data-testid='popover-content' side="bottom" onClick={handleContentClick}>
|
||||
<PopoverPrimitive.Content align={position} className="z-[9999] mt-2 origin-top-right rounded bg-white shadow-md ring-1 ring-[rgba(0,0,0,0.01)] focus:outline-none dark:bg-grey-900 dark:text-white" data-testid='popover-content' side="bottom" onClick={handleContentClick}>
|
||||
{children}
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Root>
|
||||
|
|
|
@ -13,6 +13,7 @@ module.exports = {
|
|||
md: '640px',
|
||||
lg: '1024px',
|
||||
xl: '1320px',
|
||||
xxl: '1440px',
|
||||
tablet: '860px'
|
||||
},
|
||||
colors: {
|
||||
|
|
Loading…
Add table
Reference in a new issue