pl-fe: styles

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-10-23 04:17:55 +02:00
parent 8c016637f6
commit 6de92275e0
7 changed files with 138 additions and 98 deletions

View File

@ -15,7 +15,6 @@ import { useGroup } from 'pl-fe/api/hooks/groups/use-group';
import { useGroupRelationship } from 'pl-fe/api/hooks/groups/use-group-relationship';
import DropdownMenu from 'pl-fe/components/dropdown-menu';
import StatusActionButton from 'pl-fe/components/status-action-button';
import HStack from 'pl-fe/components/ui/hstack';
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
import { languages } from 'pl-fe/features/preferences';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
@ -38,8 +37,6 @@ import copy from 'pl-fe/utils/copy';
import GroupPopover from './groups/popover/group-popover';
import Popover from './ui/popover';
import Stack from './ui/stack';
import Text from './ui/text';
import type { Menu } from 'pl-fe/components/dropdown-menu';
import type { Emoji as EmojiType } from 'pl-fe/features/emoji';
@ -183,18 +180,18 @@ const InteractionPopover: React.FC<IInteractionPopover> = ({ type, allowed }) =>
const allowedType = allowed?.includes('followers') ? 'followers' : allowed?.includes('following') ? 'following' : allowed?.includes('mutuals') ? 'mutuals' : 'mentioned';
return (
<Stack space={1} className='max-w-96'>
<Text weight='semibold' align='center'>
<div className='⁂-interaction-popover'>
<p className='⁂-interaction-popover__header'>
{intl.formatMessage(INTERACTION_POLICY_HEADERS[type])}
</Text>
<Text theme='muted' align='center'>
</p>
<p className='⁂-interaction-popover__description'>
{intl.formatMessage(INTERACTION_POLICY_DESCRIPTIONS[type][allowedType])}
</Text>
</Stack>
</p>
</div>
);
};
interface IActionButton extends Pick<IStatusActionBar, 'status' | 'statusActionButtonTheme' | 'withLabels'> {
interface IActionButton extends Pick<IStatusActionBar, 'status' | 'withLabels'> {
me: Me;
onOpenUnauthorizedModal: (action?: UnauthorizedModalAction) => void;
}
@ -205,7 +202,6 @@ interface IReplyButton extends IActionButton {
const ReplyButton: React.FC<IReplyButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
onOpenUnauthorizedModal,
@ -247,7 +243,6 @@ const ReplyButton: React.FC<IReplyButton> = ({
count={status.replies_count}
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
disabled={replyDisabled}
theme={statusActionButtonTheme}
/>
);
@ -276,7 +271,6 @@ interface IReblogButton extends IActionButton {
const ReblogButton: React.FC<IReblogButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
onOpenUnauthorizedModal,
@ -330,8 +324,8 @@ const ReblogButton: React.FC<IReblogButton> = ({
const reblogButton = (
<StatusActionButton
className='⁂-status-action-bar__button--reblog'
icon={reblogIcon}
color='success'
disabled={!publicStatus}
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
active={status.reblogged}
@ -339,7 +333,6 @@ const ReblogButton: React.FC<IReblogButton> = ({
onLongPress={handleReblogLongPress}
count={status.reblogs_count + status.quotes_count}
text={withLabels ? intl.formatMessage(messages.reblog) : undefined}
theme={statusActionButtonTheme}
/>
);
@ -385,7 +378,6 @@ const ReblogButton: React.FC<IReblogButton> = ({
const FavouriteButton: React.FC<IActionButton> = ({
status,
statusActionButtonTheme,
me,
withLabels,
onOpenUnauthorizedModal,
@ -424,13 +416,11 @@ const FavouriteButton: React.FC<IActionButton> = ({
title={intl.formatMessage(messages.favourite)}
icon={features.statusDislikes ? require('@phosphor-icons/core/regular/thumbs-up.svg') : require('@phosphor-icons/core/regular/star.svg')}
filledIcon={features.statusDislikes ? require('@phosphor-icons/core/fill/thumbs-up-fill.svg') : require('@phosphor-icons/core/fill/star-fill.svg')}
color='accent'
onClick={handleFavouriteClick}
onLongPress={handleFavouriteLongPress}
active={status.favourited}
count={status.favourites_count}
text={withLabels ? intl.formatMessage(messages.favourite) : undefined}
theme={statusActionButtonTheme}
/>
);
@ -447,7 +437,6 @@ const FavouriteButton: React.FC<IActionButton> = ({
const DislikeButton: React.FC<IActionButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
onOpenUnauthorizedModal,
@ -483,13 +472,11 @@ const DislikeButton: React.FC<IActionButton> = ({
title={intl.formatMessage(messages.disfavourite)}
icon={require('@phosphor-icons/core/regular/thumbs-down.svg')}
filledIcon={require('@phosphor-icons/core/fill/thumbs-down-fill.svg')}
color='accent'
onClick={handleDislikeClick}
onLongPress={handleDislikeLongPress}
active={status.disliked}
count={status.dislikes_count}
text={withLabels ? intl.formatMessage(messages.disfavourite) : undefined}
theme={statusActionButtonTheme}
/>
);
};
@ -498,7 +485,6 @@ const getLongerWrench = (emojis: Array<CustomEmoji>) => emojis.find(({ shortcode
const WrenchButton: React.FC<IActionButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
}) => {
@ -536,19 +522,16 @@ const WrenchButton: React.FC<IActionButton> = ({
title={intl.formatMessage(messages.wrench)}
icon={require('@phosphor-icons/core/regular/wrench.svg')}
filledIcon={require('@phosphor-icons/core/fill/wrench-fill.svg')}
color='accent'
onClick={handleWrenchClick}
onLongPress={handleWrenchLongPress}
active={wrenches?.me}
count={wrenches?.count || undefined}
theme={statusActionButtonTheme}
/>
);
};
const EmojiPickerButton: React.FC<Omit<IActionButton, 'onOpenUnauthorizedModal'>> = ({
status,
statusActionButtonTheme,
withLabels,
me,
}) => {
@ -562,10 +545,7 @@ const EmojiPickerButton: React.FC<Omit<IActionButton, 'onOpenUnauthorizedModal'>
};
return me && !withLabels && features.emojiReacts && (
<EmojiPickerDropdown
onPickEmoji={handlePickEmoji}
theme={statusActionButtonTheme}
/>
<EmojiPickerDropdown onPickEmoji={handlePickEmoji} />
);
};
@ -577,7 +557,6 @@ interface IMenuButton extends IActionButton {
const MenuButton: React.FC<IMenuButton> = ({
status,
statusActionButtonTheme,
me,
expandable,
fromBookmarks,
@ -1111,10 +1090,9 @@ const MenuButton: React.FC<IMenuButton> = ({
<StatusActionButton
title={intl.formatMessage(messages.more)}
icon={require('@phosphor-icons/core/regular/dots-three.svg')}
theme={statusActionButtonTheme}
/>
</DropdownMenu>
), [menu, statusActionButtonTheme]);
), [menu]);
};
interface IStatusActionBar {
@ -1123,7 +1101,6 @@ interface IStatusActionBar {
withLabels?: boolean;
expandable?: boolean;
space?: 'sm' | 'md' | 'lg';
statusActionButtonTheme?: 'default' | 'inverse';
fromBookmarks?: boolean;
}
@ -1132,7 +1109,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
withLabels = false,
expandable,
space = 'sm',
statusActionButtonTheme = 'default',
fromBookmarks = false,
rebloggedBy,
}) => {
@ -1156,25 +1132,13 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
return null;
}
const spacing: {
[key: string]: React.ComponentProps<typeof HStack>['space'];
} = {
'sm': 2,
'md': 8,
'lg': 0, // using justifyContent instead on the HStack
};
return (
<HStack
justifyContent={space === 'lg' ? 'between' : undefined}
space={spacing[space]}
grow={space === 'lg'}
<div
className={`⁂-status-action-bar ⁂-status-action-bar--${space}`}
onClick={onContainerClick}
alignItems='center'
>
<ReplyButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
@ -1183,7 +1147,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
<ReblogButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
@ -1192,7 +1155,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
<FavouriteButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
@ -1200,7 +1162,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
<DislikeButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
@ -1208,7 +1169,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
<WrenchButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
@ -1216,14 +1176,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
<EmojiPickerButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
/>
<MenuButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
@ -1231,7 +1189,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
fromBookmarks={fromBookmarks}
publicStatus={publicStatus}
/>
</HStack>
</div>
);
};

View File

@ -8,13 +8,6 @@ import { useSettings } from 'pl-fe/stores/settings';
import AnimatedNumber from './animated-number';
const COLORS = {
accent: 'accent',
success: 'success',
};
type Color = keyof typeof COLORS;
interface IStatusActionCounter {
count: number;
}
@ -36,14 +29,12 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonEleme
filledIcon?: string;
count?: number;
active?: boolean;
color?: Color;
text?: React.ReactNode;
theme?: 'default' | 'inverse';
onLongPress?: (event: React.MouseEvent | React.TouchEvent) => void;
}
const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButton>((props, ref): JSX.Element => {
const { icon, filledIcon, className, iconClassName, active, color, count = 0, text, theme = 'default', onLongPress, ...filteredProps } = props;
const { icon, filledIcon, className, iconClassName, active, count = 0, text, onLongPress, ...filteredProps } = props;
const longPressBind = useLongPress((e) => {
if (!onLongPress || e.type !== 'touchstart') return;
@ -66,9 +57,9 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
const renderText = () => {
if (text) {
return (
<Text tag='span' theme='inherit' size='sm'>
<span className='⁂-status-action-bar__button__text'>
{text}
</Text>
</span>
);
} else if (count) {
return (
@ -82,16 +73,9 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
ref={ref}
type='button'
className={clsx(
'-m-1 flex items-center rounded-full p-2 rtl:space-x-reverse',
'transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0',
'⁂-status-action-bar__button',
{
'text-gray-600 hover:text-gray-800 dark:hover:text-white bg-transparent hover:bg-primary-100 dark:hover:bg-primary-800 black:hover:bg-gray-800': theme === 'default',
'text-white/80 hover:text-white bg-transparent dark:bg-transparent': theme === 'inverse',
'hover:text-gray-600 dark:hover:text-white': !filteredProps.disabled,
'text-accent-300 hover:text-accent-300 dark:hover:text-accent-300': active && color === COLORS.accent,
'text-success-600 dark:text-success-400 hover:text-success-600 dark:hover:text-success-400': active && color === COLORS.success,
'space-x-1': !text,
'space-x-2': text,
'⁂-status-action-bar__button--active': active,
},
className,
)}

View File

@ -4,9 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts';
import Emoji from 'pl-fe/components/ui/emoji';
import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Text from 'pl-fe/components/ui/text';
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
import unicodeMapping from 'pl-fe/features/emoji/mapping';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
@ -79,12 +77,8 @@ const StatusReaction: React.FC<IStatusReaction> = ({ reaction, statusId, obfusca
return (
<button
className={clsx('group flex cursor-pointer items-center gap-2 overflow-hidden rounded-md border border-gray-400 px-1.5 py-1 transition-all', {
'bg-primary-100 dark:border-primary-500 dark:bg-primary-500 dark:ring-2 dark:ring-primary-300 black:border-primary-600 black:bg-primary-600': reaction.me,
'bg-transparent dark:border-primary-700 dark:bg-primary-700 black:border-primary-800 black:bg-primary-800': !reaction.me,
'cursor-pointer': !unauthenticated,
'hover:bg-primary-200 hover:dark:border-primary-300 hover:dark:bg-primary-300 hover:black:bg-primary-500': reaction.me && !unauthenticated,
'hover:bg-primary-100 hover:dark:border-primary-600 hover:dark:bg-primary-600 hover:black:bg-primary-700': !reaction.me && !unauthenticated,
className={clsx('⁂-status-reactions-bar__button', {
'⁂-status-reactions-bar__button--active': reaction.me,
})}
key={reaction.name}
onClick={handleClick}
@ -95,11 +89,11 @@ const StatusReaction: React.FC<IStatusReaction> = ({ reaction, statusId, obfusca
disabled={unauthenticated}
{...bind}
>
<Emoji className='h-5 max-w-48 transition-transform group-hover:scale-125' emoji={reaction.name} src={reaction.url || undefined} />
<Emoji emoji={reaction.name} src={reaction.url || undefined} />
<Text size='xs' weight='semibold' theme='inherit'>
<p>
<AnimatedNumber value={reaction.count} obfuscate={obfuscate} short />
</Text>
</p>
</button>
);
};
@ -121,7 +115,7 @@ const StatusReactionsBar: React.FC<IStatusReactionsBar> = ({ status, collapsed }
const sortedReactions = status.emoji_reactions.toSorted((a, b) => (b.count || 0) - (a.count || 0));
return (
<HStack className='pt-2' space={2} wrap>
<div className='⁂-status-reactions-bar'>
{sortedReactions.map((reaction) => reaction.count ? (
<StatusReaction
key={reaction.name}
@ -134,17 +128,14 @@ const StatusReactionsBar: React.FC<IStatusReactionsBar> = ({ status, collapsed }
{me && (
<EmojiPickerDropdown onPickEmoji={handlePickEmoji}>
<button
className='emoji-picker-dropdown cursor-pointer rounded-md border border-gray-400 bg-transparent p-1.5 transition-colors hover:bg-gray-50 black:border-primary-800 black:bg-primary-800 hover:black:bg-primary-700 dark:border-primary-700 dark:bg-primary-700 hover:dark:border-primary-600 hover:dark:bg-primary-600'
className='⁂-status-reactions-bar__picker-button emoji-picker-dropdown'
title={intl.formatMessage(messages.addEmoji)}
>
<Icon
className='size-4'
src={require('@phosphor-icons/core/regular/smiley-sticker.svg')}
/>
<Icon src={require('@phosphor-icons/core/regular/smiley-sticker.svg')} />
</button>
</EmojiPickerDropdown>
)}
</HStack>
</div>
);
};

View File

@ -215,7 +215,7 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
};
return (
<div className='media-modal pointer-events-auto fixed inset-0 z-[9999] h-full bg-gray-900/90'>
<div className='⁂-media-modal media-modal pointer-events-auto fixed inset-0 z-[9999] h-full bg-gray-900/90'>
<div
className='absolute inset-0'
role='presentation'
@ -314,7 +314,6 @@ const MediaModal: React.FC<MediaModalProps & BaseModalProps> = (props) => {
<StatusActionBar
status={status}
space='md'
statusActionButtonTheme='inverse'
expandable
/>
</HStack>

View File

@ -1,4 +1,4 @@
@mixin text($family: sans, $size: md, $theme: default, $tracking: normal, $transform: normal, $truncate: false, $weight: normal) {
@mixin text($family: sans, $size: md, $theme: default, $tracking: normal, $transform: normal, $truncate: false, $weight: normal, $align: none) {
@if $family == sans {
font-family: var(--font-sans);
} @else if $family == mono {
@ -88,6 +88,14 @@
} @else {
@warn "Unknown font weight `#{$weight}`.";
}
@if $align == left {
text-align: left;
} @else if $align == center {
text-align: center;
} @else if $align == right {
text-align: right;
}
}
@mixin card($variant: default, $size: md) {

View File

@ -1,4 +1,4 @@
@use 'mixins';
@use 'mixins';
.-notification {
display: flex;

View File

@ -1,3 +1,5 @@
@use 'mixins';
.-status {
@apply cursor-pointer;
@ -6,6 +8,86 @@
}
}
.-status-reactions-bar {
@apply flex gap-2 flex-wrap pt-2;
&__button {
@apply flex cursor-pointer items-center gap-2 overflow-hidden rounded-md border border-gray-400 px-1.5 py-1 transition-all bg-transparent dark:border-primary-700 dark:bg-primary-700 black:border-primary-800 black:bg-primary-800;
&:not(:disabled) {
@apply hover:bg-primary-100 hover:dark:border-primary-600 hover:dark:bg-primary-600 hover:black:bg-primary-700 cursor-pointer;
}
&--active {
@apply bg-primary-100 dark:border-primary-500 dark:bg-primary-500 dark:ring-2 dark:ring-primary-300 black:border-primary-600 black:bg-primary-600;
&:not(:disabled) {
@apply hover:bg-primary-200 hover:dark:border-primary-300 hover:dark:bg-primary-300 hover:black:bg-primary-500;
}
}
img, svg {
@apply h-5 max-w-48 transition-transform;
}
&:hover img, &:hover svg {
@apply scale-125;
}
p {
@include mixins.text($theme: inherit, $size: xs, $weight: semibold);
}
}
&__picker-button {
@apply cursor-pointer rounded-md border border-gray-400 bg-transparent p-1.5 transition-colors hover:bg-gray-50 black:border-primary-800 black:bg-primary-800 hover:black:bg-primary-700 dark:border-primary-700 dark:bg-primary-700 hover:dark:border-primary-600 hover:dark:bg-primary-600;
svg {
@apply size-4;
}
}
}
.-status-action-bar {
@apply flex items-center gap-2;
&--md {
@apply gap-8;
}
&--lg {
@apply justify-between grow;
}
&__button {
@apply -m-1 flex items-center rounded-full p-2 rtl:space-x-reverse transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0 text-gray-600 hover:text-gray-800 dark:hover:text-white bg-transparent hover:bg-primary-100 dark:hover:bg-primary-800 black:hover:bg-gray-800;
&:not(:disabled) {
@apply hover:text-gray-600 dark:hover:text-white;
}
span {
@include mixins.text($theme: inherit, $size: sm);
}
&:has(.-status-action-bar__button__text) {
@apply gap-2;
}
&:not(:has(.-status-action-bar__button__text)) {
@apply gap-1;
}
&--active {
@apply text-accent-300 hover:text-accent-300 dark:hover:text-accent-300;
&.-status-action-bar__button--reblog {
@apply text-success-600 dark:text-success-400 hover:text-success-600 dark:hover:text-success-400;
}
}
}
}
.thread__status {
.-status__wrapper {
padding: 0;
@ -109,4 +191,22 @@
display: none;
}
}
}
.-interaction-popover {
@apply flex flex-col gap-1 max-w-96;
&__header {
@include mixins.text($weight: semibold, $align: center);
}
&__description {
@include mixins.text($theme: muted, $align: center);
}
}
.-media-modal {
.-status-action-bar__button {
@apply text-white/80 hover:text-white bg-transparent dark:bg-transparent;
}
}