pl-fe: @react-spring/web migrations, adapted from mastodon
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
import { animated, type AnimatedProps } from '@react-spring/web';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
@ -13,7 +14,7 @@ interface IReaction {
|
||||
announcementId: string;
|
||||
reaction: AnnouncementReaction;
|
||||
emojiMap: Record<string, CustomEmoji>;
|
||||
style: React.CSSProperties;
|
||||
style: AnimatedProps<React.ComponentProps<'button'>>['style'];
|
||||
}
|
||||
|
||||
const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, style }) => {
|
||||
@ -42,7 +43,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, sty
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
<animated.button
|
||||
className={clsx('flex shrink-0 items-center gap-1.5 rounded-sm bg-gray-100 px-1.5 py-1 transition-colors dark:bg-primary-900', {
|
||||
'bg-gray-200 dark:bg-primary-800': hovered,
|
||||
'bg-primary-200 dark:bg-primary-500': reaction.me,
|
||||
@ -59,7 +60,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, emojiMap, sty
|
||||
<span className='block min-w-[9px] text-center text-xs font-medium text-primary-600 dark:text-white'>
|
||||
<AnimatedNumber value={reaction.count} />
|
||||
</span>
|
||||
</button>
|
||||
</animated.button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useTransition } from '@react-spring/web';
|
||||
import React from 'react';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { useAnnouncements } from 'pl-fe/queries/announcements/use-announcements';
|
||||
@ -24,36 +24,36 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, emoj
|
||||
addReaction({ announcementId, name: (data as NativeEmoji).native.replace(/:/g, '') });
|
||||
};
|
||||
|
||||
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
||||
|
||||
const willLeave = () => ({ scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) });
|
||||
|
||||
const visibleReactions = reactions.filter(x => x.count > 0);
|
||||
|
||||
const styles = visibleReactions.map(reaction => ({
|
||||
key: reaction.name,
|
||||
data: reaction,
|
||||
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
||||
}));
|
||||
const transitions = useTransition(visibleReactions, {
|
||||
from: {
|
||||
scale: 0,
|
||||
},
|
||||
enter: {
|
||||
scale: 1,
|
||||
},
|
||||
leave: {
|
||||
scale: 0,
|
||||
},
|
||||
immediate: reduceMotion,
|
||||
keys: visibleReactions.map(x => x.name),
|
||||
});
|
||||
|
||||
return (
|
||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||
{items => (
|
||||
<div className='flex flex-wrap items-center gap-1'>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<Reaction
|
||||
key={key}
|
||||
reaction={data}
|
||||
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
||||
announcementId={announcementId}
|
||||
emojiMap={emojiMap}
|
||||
/>
|
||||
))}
|
||||
<div className='flex flex-wrap items-center gap-1'>
|
||||
{transitions(({ scale }, reaction) => (
|
||||
<Reaction
|
||||
key={reaction.name}
|
||||
reaction={reaction}
|
||||
style={{ transform: scale.to((s) => `scale(${s})`) }}
|
||||
announcementId={announcementId}
|
||||
emojiMap={emojiMap}
|
||||
/>
|
||||
))}
|
||||
|
||||
{visibleReactions.length < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||
</div>
|
||||
)}
|
||||
</TransitionMotion>
|
||||
{visibleReactions.length < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { animated, config, useSpring } from '@react-spring/web';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Motion, presets, spring } from 'react-motion';
|
||||
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
@ -16,16 +16,20 @@ const messages = defineMessages({
|
||||
votes: { id: 'poll.votes', defaultMessage: '{votes, plural, one {# vote} other {# votes}}' },
|
||||
});
|
||||
|
||||
const PollPercentageBar: React.FC<{ percent: number; leading: boolean }> = ({ percent, leading }): JSX.Element => (
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { ...presets.gentle, precision: 0.1 }) }}>
|
||||
{({ width }) => (
|
||||
<span
|
||||
className='absolute inset-0 inline-block h-full rounded-l-md bg-primary-100 dark:bg-primary-500'
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
const PollPercentageBar: React.FC<{ percent: number; leading: boolean }> = ({ percent, leading }): JSX.Element => {
|
||||
const styles = useSpring({
|
||||
from: { width: '0%' },
|
||||
to: { width: `${percent}%` },
|
||||
config: config.gentle,
|
||||
});
|
||||
|
||||
return (
|
||||
<animated.span
|
||||
className='absolute inset-0 inline-block h-full rounded-l-md bg-primary-100 dark:bg-primary-500'
|
||||
style={styles}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface IPollOptionText extends IPollOption {
|
||||
percent: number;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { animated, useSpring } from '@react-spring/web';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import Motion from 'pl-fe/features/ui/util/optional-motion';
|
||||
import { useSettings } from 'pl-fe/stores/settings';
|
||||
|
||||
interface IProgressBar {
|
||||
/** Number between 0 and 1 to represent the percentage complete. */
|
||||
@ -12,22 +12,29 @@ interface IProgressBar {
|
||||
}
|
||||
|
||||
/** A horizontal meter filled to the given percentage. */
|
||||
const ProgressBar: React.FC<IProgressBar> = ({ progress, size = 'md' }) => (
|
||||
<div
|
||||
className={clsx('h-2.5 w-full overflow-hidden rounded-lg bg-gray-300 dark:bg-primary-800', {
|
||||
'h-2.5': size === 'md',
|
||||
'h-[6px]': size === 'sm',
|
||||
})}
|
||||
>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress * 100) }}>
|
||||
{({ width }) => (
|
||||
<div
|
||||
className='h-full bg-secondary-500'
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
);
|
||||
const ProgressBar: React.FC<IProgressBar> = ({ progress, size = 'md' }) => {
|
||||
const { reduceMotion } = useSettings();
|
||||
|
||||
const styles = useSpring({
|
||||
from: { width: '0%' },
|
||||
to: { width: `${progress}%` },
|
||||
reset: true,
|
||||
immediate: reduceMotion,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('h-2.5 w-full overflow-hidden rounded-lg bg-gray-300 dark:bg-primary-800', {
|
||||
'h-2.5': size === 'md',
|
||||
'h-[6px]': size === 'sm',
|
||||
})}
|
||||
>
|
||||
<animated.div
|
||||
className='h-full bg-secondary-500'
|
||||
style={styles}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProgressBar as default };
|
||||
|
||||
@ -20,18 +20,18 @@ import presentationIcon from '@phosphor-icons/core/regular/presentation.svg';
|
||||
import spreadsheetIcon from '@phosphor-icons/core/regular/table.svg';
|
||||
import videoIcon from '@phosphor-icons/core/regular/video.svg';
|
||||
import xIcon from '@phosphor-icons/core/regular/x.svg';
|
||||
import { animated, config, useSpring } from '@react-spring/web';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import AltIndicator from 'pl-fe/components/alt-indicator';
|
||||
import Blurhash from 'pl-fe/components/blurhash';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import IconButton from 'pl-fe/components/ui/icon-button';
|
||||
import Motion from 'pl-fe/features/ui/util/optional-motion';
|
||||
import { useModalsActions } from 'pl-fe/stores/modals';
|
||||
import { useSettings } from 'pl-fe/stores/settings';
|
||||
|
||||
import type { MediaAttachment } from 'pl-api';
|
||||
|
||||
@ -103,6 +103,7 @@ const Upload: React.FC<IUpload> = ({
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { openModal } = useModalsActions();
|
||||
const { reduceMotion } = useSettings();
|
||||
|
||||
const handleUndoClick: React.MouseEventHandler = e => {
|
||||
if (onDelete) {
|
||||
@ -142,6 +143,15 @@ const Upload: React.FC<IUpload> = ({
|
||||
const mediaType = media.type;
|
||||
const mimeType = media.mime_type as string | undefined;
|
||||
|
||||
const styles = useSpring({
|
||||
backgroundImage: mediaType === 'image' ? `url(${media.preview_url})` : undefined,
|
||||
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined,
|
||||
from: { scale: 0.8 },
|
||||
to: { scale: 1 },
|
||||
config: config.stiff,
|
||||
immediate: reduceMotion,
|
||||
});
|
||||
|
||||
const uploadIcon = mediaType === 'unknown' && (
|
||||
<Icon
|
||||
className='mx-auto my-12 size-16 text-gray-800 dark:text-gray-200'
|
||||
@ -160,75 +170,67 @@ const Upload: React.FC<IUpload> = ({
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<Blurhash hash={media.blurhash} className='⁂-media-gallery__preview' />
|
||||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ scale }) => (
|
||||
<div
|
||||
className={clsx('compose-form__upload-thumbnail relative h-40 w-full overflow-hidden bg-contain bg-center bg-no-repeat', mediaType)}
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
backgroundImage: mediaType === 'image' ? `url(${media.preview_url})` : undefined,
|
||||
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined,
|
||||
}}
|
||||
>
|
||||
<HStack className='absolute right-2 top-2 z-10' space={2}>
|
||||
{onDescriptionChange && (
|
||||
<IconButton
|
||||
onClick={handleOpenAltTextModal}
|
||||
src={editIcon}
|
||||
theme='dark'
|
||||
className='hover:scale-105 hover:bg-gray-900'
|
||||
iconClassName='h-5 w-5'
|
||||
title={intl.formatMessage(messages.description)}
|
||||
/>
|
||||
)}
|
||||
{(withPreview && mediaType !== 'unknown' && Boolean(media.url)) && (
|
||||
<IconButton
|
||||
onClick={handleOpenModal}
|
||||
src={zoomInIcon}
|
||||
theme='dark'
|
||||
className='hover:scale-105 hover:bg-gray-900'
|
||||
iconClassName='h-5 w-5'
|
||||
title={intl.formatMessage(messages.preview)}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<IconButton
|
||||
onClick={handleUndoClick}
|
||||
src={xIcon}
|
||||
theme='dark'
|
||||
className='hover:scale-105 hover:bg-gray-900'
|
||||
iconClassName='h-5 w-5'
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
<animated.div
|
||||
className={clsx('compose-form__upload-thumbnail relative h-40 w-full overflow-hidden bg-contain bg-center bg-no-repeat', mediaType)}
|
||||
style={styles}
|
||||
>
|
||||
<HStack className='absolute right-2 top-2 z-10' space={2}>
|
||||
{onDescriptionChange && (
|
||||
<IconButton
|
||||
onClick={handleOpenAltTextModal}
|
||||
src={editIcon}
|
||||
theme='dark'
|
||||
className='hover:scale-105 hover:bg-gray-900'
|
||||
iconClassName='h-5 w-5'
|
||||
title={intl.formatMessage(messages.description)}
|
||||
/>
|
||||
)}
|
||||
{(withPreview && mediaType !== 'unknown' && Boolean(media.url)) && (
|
||||
<IconButton
|
||||
onClick={handleOpenModal}
|
||||
src={zoomInIcon}
|
||||
theme='dark'
|
||||
className='hover:scale-105 hover:bg-gray-900'
|
||||
iconClassName='h-5 w-5'
|
||||
title={intl.formatMessage(messages.preview)}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<IconButton
|
||||
onClick={handleUndoClick}
|
||||
src={xIcon}
|
||||
theme='dark'
|
||||
className='hover:scale-105 hover:bg-gray-900'
|
||||
iconClassName='h-5 w-5'
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack space={2} justifyContent='between' className='absolute inset-x-2 bottom-2 z-10'>
|
||||
<span className='overflow-hidden text-ellipsis rounded bg-gray-900 px-2 py-1 text-xs font-medium text-white'>
|
||||
{media.url.split('/').at(-1)}
|
||||
</span>
|
||||
<HStack space={2} justifyContent='between' className='absolute inset-x-2 bottom-2 z-10'>
|
||||
<span className='overflow-hidden text-ellipsis rounded bg-gray-900 px-2 py-1 text-xs font-medium text-white'>
|
||||
{media.url.split('/').at(-1)}
|
||||
</span>
|
||||
|
||||
{onDescriptionChange && !description && (
|
||||
<button onClick={handleOpenAltTextModal}>
|
||||
<AltIndicator
|
||||
warning
|
||||
title={intl.formatMessage(messages.descriptionMissingTitle)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</HStack>
|
||||
{onDescriptionChange && !description && (
|
||||
<button onClick={handleOpenAltTextModal}>
|
||||
<AltIndicator
|
||||
warning
|
||||
title={intl.formatMessage(messages.descriptionMissingTitle)}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<div className='absolute inset-0 z-[-1] size-full'>
|
||||
{mediaType === 'video' && (
|
||||
<video className='size-full object-cover' autoPlay playsInline muted loop>
|
||||
<source src={media.preview_url} />
|
||||
</video>
|
||||
)}
|
||||
{uploadIcon}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
<div className='absolute inset-0 z-[-1] size-full'>
|
||||
{mediaType === 'video' && (
|
||||
<video className='size-full object-cover' autoPlay playsInline muted loop>
|
||||
<source src={media.preview_url} />
|
||||
</video>
|
||||
)}
|
||||
{uploadIcon}
|
||||
</div>
|
||||
</animated.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user