mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
[Glitch] Tags component
Port 3f46034039 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
.tag {
|
||||
border-radius: 9999px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
appearance: none;
|
||||
background: none;
|
||||
padding: 8px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
color: var(--color-text-primary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
button.tag:hover,
|
||||
button.tag:focus {
|
||||
border-color: var(--color-bg-brand-base-hover);
|
||||
}
|
||||
|
||||
button.tag:focus-visible {
|
||||
outline: var(--outline-focus-default);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.active {
|
||||
border-color: var(--color-text-brand);
|
||||
background: var(--color-bg-brand-softer);
|
||||
color: var(--color-text-brand);
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
svg {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.active .closeButton {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.tagsWrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import MusicNoteIcon from '@/material-icons/400-24px/music_note.svg?react';
|
||||
|
||||
import { EditableTag, Tag } from './tag';
|
||||
|
||||
const meta = {
|
||||
component: Tag,
|
||||
title: 'Components/Tags/Single Tag',
|
||||
args: {
|
||||
name: 'example-tag',
|
||||
active: false,
|
||||
onClick: action('Click'),
|
||||
},
|
||||
} satisfies Meta<typeof Tag>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
icon: MusicNoteIcon,
|
||||
},
|
||||
};
|
||||
|
||||
export const Editable: Story = {
|
||||
render(args) {
|
||||
return <EditableTag {...args} onRemove={action('Remove')} />;
|
||||
},
|
||||
};
|
||||
|
||||
export const EditableWithIcon: Story = {
|
||||
render(args) {
|
||||
return (
|
||||
<EditableTag
|
||||
{...args}
|
||||
removeIcon={MusicNoteIcon}
|
||||
onRemove={action('Remove')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
94
app/javascript/flavours/glitch/components/tags/tag.tsx
Normal file
94
app/javascript/flavours/glitch/components/tags/tag.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import type { IconProp } from '../icon';
|
||||
import { Icon } from '../icon';
|
||||
import { IconButton } from '../icon_button';
|
||||
|
||||
import classes from './style.module.css';
|
||||
|
||||
export interface TagProps {
|
||||
name: ReactNode;
|
||||
active?: boolean;
|
||||
icon?: IconProp;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Tag = forwardRef<
|
||||
HTMLButtonElement,
|
||||
TagProps & ComponentPropsWithoutRef<'button'>
|
||||
>(({ name, active, icon, className, children, ...props }, ref) => {
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
type='button'
|
||||
ref={ref}
|
||||
className={classNames(className, classes.tag, active && classes.active)}
|
||||
>
|
||||
{icon && <Icon icon={icon} id='tag-icon' className={classes.icon} />}
|
||||
{typeof name === 'string' ? `#${name}` : name}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
Tag.displayName = 'Tag';
|
||||
|
||||
export const EditableTag = forwardRef<
|
||||
HTMLSpanElement,
|
||||
TagProps & {
|
||||
onRemove: () => void;
|
||||
removeIcon?: IconProp;
|
||||
} & ComponentPropsWithoutRef<'span'>
|
||||
>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
active,
|
||||
icon,
|
||||
className,
|
||||
children,
|
||||
removeIcon = CloseIcon,
|
||||
onRemove,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<span
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={classNames(className, classes.tag, active && classes.active)}
|
||||
>
|
||||
{icon && <Icon icon={icon} id='tag-icon' className={classes.icon} />}
|
||||
{typeof name === 'string' ? `#${name}` : name}
|
||||
{children}
|
||||
<IconButton
|
||||
className={classes.closeButton}
|
||||
iconComponent={removeIcon}
|
||||
onClick={onRemove}
|
||||
icon='remove'
|
||||
title={intl.formatMessage({
|
||||
id: 'tag.remove',
|
||||
defaultMessage: 'Remove',
|
||||
})}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
);
|
||||
EditableTag.displayName = 'EditableTag';
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import { Tags } from './tags';
|
||||
|
||||
const meta = {
|
||||
component: Tags,
|
||||
title: 'Components/Tags/List',
|
||||
args: {
|
||||
tags: [{ name: 'tag-one' }, { name: 'tag-two' }],
|
||||
active: 'tag-one',
|
||||
},
|
||||
} satisfies Meta<typeof Tags>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render(args) {
|
||||
return <Tags {...args} />;
|
||||
},
|
||||
};
|
||||
|
||||
export const Editable: Story = {
|
||||
args: {
|
||||
onRemove: action('Remove'),
|
||||
},
|
||||
};
|
||||
54
app/javascript/flavours/glitch/components/tags/tags.tsx
Normal file
54
app/javascript/flavours/glitch/components/tags/tags.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||
|
||||
import classes from './style.module.css';
|
||||
import { EditableTag, Tag } from './tag';
|
||||
import type { TagProps } from './tag';
|
||||
|
||||
type Tag = TagProps & { name: string };
|
||||
|
||||
export type TagsProps = {
|
||||
tags: Tag[];
|
||||
active?: string;
|
||||
} & (
|
||||
| ({
|
||||
onRemove?: never;
|
||||
} & ComponentPropsWithoutRef<'button'>)
|
||||
| ({ onRemove?: (tag: string) => void } & ComponentPropsWithoutRef<'span'>)
|
||||
);
|
||||
|
||||
export const Tags: FC<TagsProps> = ({ tags, active, onRemove, ...props }) => {
|
||||
if (onRemove) {
|
||||
return (
|
||||
<div className={classes.tagsWrapper}>
|
||||
{tags.map((tag) => (
|
||||
<MappedTag
|
||||
key={tag.name}
|
||||
active={tag.name === active}
|
||||
onRemove={onRemove}
|
||||
{...tag}
|
||||
{...props}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.tagsWrapper}>
|
||||
{tags.map((tag) => (
|
||||
<Tag key={tag.name} active={tag.name === active} {...tag} {...props} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MappedTag: FC<Tag & { onRemove?: (tag: string) => void }> = ({
|
||||
onRemove,
|
||||
...props
|
||||
}) => {
|
||||
const handleRemove = useCallback(() => {
|
||||
onRemove?.(props.name);
|
||||
}, [onRemove, props.name]);
|
||||
return <EditableTag {...props} onRemove={handleRemove} />;
|
||||
};
|
||||
Reference in New Issue
Block a user