feat(ui): add Tag component

Adds a reusable `Tag` component with support for default and inverted styles. Also includes cleanup of unused dependencies in `package.json`.
This commit is contained in:
Brian McGee
2025-06-20 09:23:03 +01:00
parent 4586b0d17d
commit 2ed5d29c89
4 changed files with 138 additions and 3 deletions

View File

@@ -28,10 +28,8 @@
"@storybook/addon-a11y": "^9.0.8",
"@storybook/addon-docs": "^9.0.8",
"@storybook/addon-links": "^9.0.8",
"@storybook/addon-onboarding": "^9.0.8",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/addon-vitest": "^9.0.8",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^22.15.19",
"@types/three": "^0.176.0",
"@typescript-eslint/parser": "^8.32.1",
@@ -68,7 +66,6 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@solidjs/testing-library": "^0.8.10",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"solid-js": "^1.9.7",

View File

@@ -0,0 +1,37 @@
span.tag {
@apply flex items-center gap-1 w-fit px-2 py-1 rounded-full;
@apply bg-def-4;
&:focus-visible {
@apply bg-def-acc-3 outline-none;
box-shadow:
0 0 0 0.0625rem theme(colors.off.white),
0 0 0 0.125rem theme(colors.border.semantic.info.1);
}
&.active {
@apply bg-def-acc-4;
}
&.inverted {
@apply bg-inv-1;
}
&.has-action {
@apply pr-1.5;
&:hover {
@apply bg-def-acc-3;
}
&.inverted:hover {
@apply bg-inv-acc-3;
}
}
& > .icon {
&:hover {
@apply cursor-pointer;
}
}
}

View File

@@ -0,0 +1,47 @@
import { Tag, TagProps } from "@/src/components/v2/Tag/Tag";
import { Meta, type StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { expect, fn } from "storybook/test";
const meta: Meta<TagProps> = {
title: "Components/Tag",
component: Tag,
};
export default meta;
type Story = StoryObj<TagProps>;
export const Default: Story = {
args: {
label: "Label",
},
};
export const WithAction: Story = {
args: {
...Default.args,
action: {
icon: "Close",
onClick: fn(),
},
},
play: async ({ canvas, step, userEvent, args }: StoryContext) => {
await userEvent.click(canvas.getByRole("button"));
await expect(args.action.onClick).toHaveBeenCalled();
},
};
export const Inverted: Story = {
args: {
label: "Label",
inverted: true,
},
};
export const InvertedWithAction: Story = {
args: {
...WithAction.args,
inverted: true,
},
play: WithAction.play,
};

View File

@@ -0,0 +1,54 @@
import "./Tag.css";
import cx from "classnames";
import { Typography } from "@/src/components/v2/Typography/Typography";
import { createSignal, Show } from "solid-js";
import Icon, { IconVariant } from "../Icon/Icon";
export interface TagAction {
icon: IconVariant;
onClick: () => void;
}
export interface TagProps {
label: string;
action?: TagAction;
inverted?: boolean;
}
export const Tag = (props: TagProps) => {
const inverted = () => props.inverted || false;
const [isActive, setIsActive] = createSignal(false);
const handleActionClick = () => {
setIsActive(true);
props.action?.onClick();
setTimeout(() => setIsActive(false), 150);
};
return (
<span
class={cx("tag", {
inverted: inverted(),
active: isActive(),
"has-action": props.action,
})}
aria-label={props.label}
aria-readonly={!props.action}
>
<Typography hierarchy="label" size="xs" inverted={inverted()}>
{props.label}
</Typography>
<Show when={props.action}>
<Icon
role="button"
icon={props.action!.icon}
size="0.5rem"
inverted={inverted()}
onClick={handleActionClick}
/>
</Show>
</span>
);
};