Merge pull request 'add Tag and TagGroup components' (#4038) from ui/tags into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4038
This commit is contained in:
brianmcgee
2025-06-24 08:30:06 +00:00
10 changed files with 212 additions and 10 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

@@ -1,4 +1,4 @@
span.tag-status {
span.machine-status {
@apply flex items-center gap-1;
.indicator {

View File

@@ -1,10 +1,10 @@
import {
MachineStatus,
TagStatusProps,
MachineStatusProps,
} from "@/src/components/v2/MachineStatus/MachineStatus";
import { Meta, StoryObj } from "@kachurun/storybook-solid";
const meta: Meta<TagStatusProps> = {
const meta: Meta<MachineStatusProps> = {
title: "Components/MachineStatus",
component: MachineStatus,
decorators: [
@@ -18,7 +18,7 @@ const meta: Meta<TagStatusProps> = {
export default meta;
type Story = StoryObj<TagStatusProps>;
type Story = StoryObj<MachineStatusProps>;
export const Online: Story = {
args: {

View File

@@ -12,14 +12,14 @@ export type MachineStatus =
| "Installed"
| "Not Installed";
export interface TagStatusProps {
export interface MachineStatusProps {
label?: boolean;
status: MachineStatus;
}
export const MachineStatus = (props: TagStatusProps) => (
export const MachineStatus = (props: MachineStatusProps) => (
<Badge
class={cx("tag-status", {
class={cx("machine-status", {
online: props.status == "Online",
offline: props.status == "Offline",
installed: props.status == "Installed",

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>
);
};

View File

@@ -0,0 +1,3 @@
div.tag-group {
@apply flex flex-wrap gap-x-1.5 gap-y-2;
}

View File

@@ -0,0 +1,43 @@
import { TagGroup, TagGroupProps } from "@/src/components/v2/TagGroup/TagGroup";
import { Meta, StoryObj } from "@kachurun/storybook-solid";
const meta: Meta<TagGroupProps> = {
title: "Components/TagGroup",
component: TagGroup,
decorators: [
(Story: StoryObj) => (
/* for some reason w-x from tailwind was not working */
<div style="width: 196px">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<TagGroupProps>;
export const Default: Story = {
args: {
labels: [
"Tag 1",
"Tag 2",
"Tag 3",
"Tag 4",
"Tag 5",
"Tag 6",
"Tag 7",
"Tag 8",
"Tag 9",
"Tag 10",
],
},
};
export const Inverted: Story = {
args: {
...Default.args,
inverted: true,
},
};

View File

@@ -0,0 +1,21 @@
import "./TagGroup.css";
import cx from "classnames";
import { For } from "solid-js";
import { Tag } from "@/src/components/v2/Tag/Tag";
export interface TagGroupProps {
labels: string[];
inverted?: boolean;
}
export const TagGroup = (props: TagGroupProps) => {
const inverted = () => props.inverted || false;
return (
<div class={cx("tag-group", { inverted: inverted() })}>
<For each={props.labels}>
{(label) => <Tag label={label} inverted={inverted()} />}
</For>
</div>
);
};