light color for hover popups

This commit is contained in:
Dylan Knutson
2025-03-02 01:41:37 +00:00
parent da422ea3aa
commit 171b2a72c2
4 changed files with 143 additions and 68 deletions

View File

@@ -1,7 +1,10 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { useHoverPreview } from '../utils/hoverPreviewUtils';
import {
useHoverPreview,
getPreviewContainerClassName,
} from '../utils/hoverPreviewUtils';
export interface HoverPreviewProps {
children: ReactNode;
@@ -41,13 +44,7 @@ export const HoverPreview: React.FC<HoverPreviewProps> = ({
createPortal(
<div
ref={previewRef}
className={[
`max-w-[${maxWidth}] max-h-[${maxHeight}] rounded-lg border`,
'border-slate-100 bg-white dark:border-slate-600',
'divide-y divide-slate-100 dark:divide-slate-600',
'shadow-xl shadow-slate-700/50',
previewClassName,
].join(' ')}
className={`${getPreviewContainerClassName(maxWidth, maxHeight)} ${previewClassName}`}
style={{
...previewStyle,
}}

View File

@@ -1,5 +1,14 @@
import * as React from 'react';
import { HoverPreview } from './HoverPreview';
import {
getHeaderFooterClassName,
getContentClassName,
getHeaderTextClassName,
getSecondaryTextClassName,
getBorderClassName,
getIconClassName,
getAvatarImageClassName,
} from '../utils/hoverPreviewUtils';
interface PostHoverPreviewProps {
children: React.ReactNode;
@@ -20,37 +29,33 @@ export const PostHoverPreview: React.FC<PostHoverPreviewProps> = ({
creatorName,
creatorAvatarPath,
}) => {
const headerFooterClassName = [
'flex items-center justify-between overflow-hidden',
'border-slate-200 bg-gradient-to-r from-white to-slate-50 p-3',
'dark:border-slate-600 dark:from-slate-800 dark:to-slate-700',
].join(' ');
// Add extra classes for PostHoverPreview's header/footer
const postHeaderFooterClassName = `${getHeaderFooterClassName()} justify-between p-3`;
const previewContent = (
<>
{/* Header: Title */}
<div className={headerFooterClassName}>
<span
className="mr-2 min-w-0 truncate text-sm font-medium tracking-tight text-slate-800 dark:text-slate-300"
title={postTitle}
>
<div className={postHeaderFooterClassName}>
<span className={getHeaderTextClassName()} title={postTitle}>
{postTitle}
</span>
</div>
{/* Image Content */}
<div className="flex items-center justify-center bg-slate-50 p-2 dark:bg-slate-900">
<div
className={`flex items-center justify-center ${getContentClassName()} p-2`}
>
{postThumbnailPath ? (
<div className="transition-transform hover:scale-[1.02]">
<img
src={postThumbnailPath}
alt={postTitle}
className="max-h-[250px] max-w-full rounded-md border border-slate-200 object-contain shadow-md dark:border-slate-600"
className={`max-h-[250px] max-w-full rounded-md ${getBorderClassName()} object-contain shadow-md`}
loading="eager"
/>
</div>
) : (
<span className="block px-4 py-6 text-sm italic text-slate-500 dark:text-slate-400">
<span className={`block px-4 py-6 ${getSecondaryTextClassName()}`}>
{postThumbnailAlt || 'No file available'}
</span>
)}
@@ -58,31 +63,31 @@ export const PostHoverPreview: React.FC<PostHoverPreviewProps> = ({
{/* Footer: Domain icon & Creator */}
{creatorName && (
<div className={headerFooterClassName}>
{(postDomainIcon && (
<img
src={postDomainIcon}
alt={postTitle}
className="h-6 w-6 rounded-md bg-slate-500 p-1 shadow-sm ring-sky-100 dark:ring-sky-900"
/>
)) || (
<span className="h-6 w-6 grow rounded-md shadow-sm ring-sky-100 dark:ring-sky-900"></span>
)}
<div className={postHeaderFooterClassName}>
<span className="flex items-center gap-2 justify-self-end">
<span
className="truncate text-xs font-medium text-slate-500 dark:text-slate-400"
title={creatorName}
>
{creatorName}
</span>
{creatorAvatarPath && (
<img
src={creatorAvatarPath}
alt={creatorName}
className="h-6 w-6 rounded-md shadow-sm ring-sky-100 dark:ring-sky-900"
className={getAvatarImageClassName('small')}
/>
)}
<span
className="truncate text-xs font-medium text-slate-700"
title={creatorName}
>
{creatorName}
</span>
</span>
{(postDomainIcon && (
<img
src={postDomainIcon}
alt={postTitle}
className={getIconClassName()}
/>
)) || (
<span className="h-6 w-6 grow rounded-md shadow-sm ring-blue-200"></span>
)}
</div>
)}
</>

View File

@@ -1,5 +1,16 @@
import * as React from 'react';
import { HoverPreview } from './HoverPreview';
import {
getHeaderFooterClassName,
getContentClassName,
getHeaderTextClassName,
getLabelTextClassName,
getValueTextClassName,
getSecondaryTextClassName,
getAvatarContainerClassName,
getAvatarImageClassName,
getIconClassName,
} from '../utils/hoverPreviewUtils';
interface UserHoverPreviewProps {
children: React.ReactNode;
@@ -24,47 +35,40 @@ export const UserHoverPreview: React.FC<UserHoverPreviewProps> = ({
userNumFollowedBy,
userNumFollowed,
}) => {
const sectionClassName = [
'flex items-center overflow-hidden',
'border-slate-200 bg-gradient-to-r from-white to-slate-50 p-2',
'dark:border-slate-600 dark:from-slate-800 dark:to-slate-700',
].join(' ');
const previewContent = (
<>
{/* Header: User Name and Domain Icon */}
<div className={`${sectionClassName} justify-between`}>
<span
className="mr-2 min-w-0 truncate text-sm font-semibold tracking-tight text-slate-800 dark:text-slate-300"
title={userName}
>
<div className={`${getHeaderFooterClassName()} justify-between`}>
<span className={getHeaderTextClassName()} title={userName}>
{userName}
</span>
{userDomainIcon && (
<img
src={userDomainIcon}
alt="Domain"
className="h-5 w-5 rounded-md bg-slate-500 p-1 shadow-sm ring-sky-100 dark:ring-sky-900"
className={getIconClassName()}
/>
)}
</div>
{/* Profile Content */}
<div className="bg-slate-50 p-3 dark:bg-slate-900">
<div className={getContentClassName()}>
<div className="flex items-center gap-4">
{/* Avatar */}
<div className="flex-shrink-0">
{userAvatarPath ? (
<div className="overflow-hidden rounded-md">
<div className={getAvatarContainerClassName()}>
<img
src={userAvatarPath}
alt={userAvatarAlt}
className="h-[80px] w-[80px] rounded-md border border-slate-200 object-cover shadow-sm dark:border-slate-600"
className={getAvatarImageClassName('medium')}
loading="eager"
/>
</div>
) : (
<span className="block h-[80px] w-[80px] rounded-md border border-slate-200 bg-slate-100 p-2 text-xs italic text-slate-500 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-400">
<span
className={`block h-[80px] w-[80px] rounded-md border border-slate-300 bg-slate-200 p-2 text-xs italic ${getSecondaryTextClassName()}`}
>
No avatar
</span>
)}
@@ -74,39 +78,35 @@ export const UserHoverPreview: React.FC<UserHoverPreviewProps> = ({
<div className="flex-grow">
<div className="grid grid-cols-2 gap-3">
<div className="text-center">
<span className="block text-sm font-semibold text-slate-700 dark:text-slate-300">
<span className={getValueTextClassName()}>
{userNumPosts ?? '-'}
</span>
<span className="text-2xs text-slate-500 dark:text-slate-400">
Posts
</span>
<span className={getLabelTextClassName()}>Posts</span>
</div>
<div className="text-center">
<span className="block text-sm font-semibold text-slate-700 dark:text-slate-300">
<span className={getValueTextClassName()}>
{userNumFollowedBy ?? '-'}
</span>
<span className="text-2xs text-slate-500 dark:text-slate-400">
Followers
</span>
<span className={getLabelTextClassName()}>Followers</span>
</div>
<div className="text-center">
<span className="block text-sm font-semibold text-slate-700 dark:text-slate-300">
<span className={getValueTextClassName()}>
{userNumFollowed ?? '-'}
</span>
<span className="text-2xs text-slate-500 dark:text-slate-400">
Following
</span>
<span className={getLabelTextClassName()}>Following</span>
</div>
<div className="text-center">
{userRegisteredAt && (
<>
<span className="text-2xs block font-medium text-slate-700 dark:text-slate-300">
<span
className={`text-2xs block font-medium text-slate-800`}
>
Member since
</span>
<span className="text-2xs leading-tight text-slate-500 dark:text-slate-400">
<span className={getLabelTextClassName()}>
{userRegisteredAt}
</span>
</>

View File

@@ -7,6 +7,79 @@ import {
useMemo,
} from 'react';
/**
* Utility functions for hover preview styling
*/
// Base preview container styling
export const getPreviewContainerClassName = (
maxWidth: string,
maxHeight: string,
) => {
return [
`max-w-[${maxWidth}] max-h-[${maxHeight}] rounded-lg`,
'border border-slate-400 bg-slate-100',
'divide-y divide-slate-300',
'shadow-lg shadow-slate-500/30',
].join(' ');
};
// Header/Footer styling
export const getHeaderFooterClassName = () => {
return [
'flex items-center overflow-hidden',
'border-slate-400 bg-gradient-to-r from-slate-200 to-slate-300 p-2',
].join(' ');
};
// Content area styling
export const getContentClassName = () => {
return 'bg-slate-100 p-3';
};
// Text styling functions
export const getHeaderTextClassName = () => {
return 'mr-2 min-w-0 truncate text-sm font-semibold text-slate-700';
};
export const getLabelTextClassName = () => {
return 'text-2xs text-slate-700';
};
export const getValueTextClassName = () => {
return 'block text-sm font-semibold text-slate-700';
};
export const getSecondaryTextClassName = () => {
return 'text-sm italic text-slate-600';
};
// Border and shadow styling
export const getBorderClassName = () => {
return 'border border-slate-300';
};
// Avatar container styling
export const getAvatarContainerClassName = () => {
return 'overflow-hidden rounded-md';
};
// Base avatar image styling
export const getAvatarImageClassName = (size: string = 'medium') => {
const sizeClasses = {
small: 'h-6 w-6',
medium: 'h-[80px] w-[80px]',
large: 'h-20 w-20',
};
return `${sizeClasses[size as keyof typeof sizeClasses]} rounded-md ${getBorderClassName()} object-cover shadow-sm`;
};
// Icon styling
export const getIconClassName = () => {
return 'h-8 w-8 rounded-md p-1';
};
/**
* Calculate the position for a hover preview popup
* @param containerRef The reference to the element containing the hover trigger