status tag component and implmentation

This commit is contained in:
Oliver Bryan
2026-01-10 16:47:42 +00:00
parent 364e4e0f64
commit 593e155755
5 changed files with 70 additions and 31 deletions

View File

@@ -4,8 +4,10 @@ import { useEffect, useState } from "react";
import { useSession } from "@/components/session-provider"; import { useSession } from "@/components/session-provider";
import SmallUserDisplay from "@/components/small-user-display"; import SmallUserDisplay from "@/components/small-user-display";
import { StatusSelect } from "@/components/status-select"; import { StatusSelect } from "@/components/status-select";
import StatusTag from "@/components/status-tag";
import { TimerModal } from "@/components/timer-modal"; import { TimerModal } from "@/components/timer-modal";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { SelectTrigger } from "@/components/ui/select";
import { UserSelect } from "@/components/user-select"; import { UserSelect } from "@/components/user-select";
import { issue } from "@/lib/server"; import { issue } from "@/lib/server";
import { issueID } from "@/lib/utils"; import { issueID } from "@/lib/utils";
@@ -84,9 +86,23 @@ export function IssueDetailPane({
</div> </div>
<div className="flex flex-col w-full p-2 py-2 gap-2"> <div className="flex flex-col w-full p-2 py-2 gap-2">
<div className="flex gap-2 -mt-1 -ml-1"> <div className="flex gap-2">
<StatusSelect statuses={statuses} value={status} onChange={handleStatusChange} /> <StatusSelect
<div className="flex w-full h-8 border-b items-center min-w-0"> statuses={statuses}
value={status}
onChange={handleStatusChange}
trigger={({ isOpen, value }) => (
<SelectTrigger
className="group w-auto flex items-center"
variant="unstyled"
chevronClassName="hidden"
isOpen={isOpen}
>
<StatusTag status={value} className="group-hover:bg-foreground/75" />
</SelectTrigger>
)}
/>
<div className="flex w-full items-center min-w-0">
<span className="block w-full truncate">{issueData.Issue.title}</span> <span className="block w-full truncate">{issueData.Issue.title}</span>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
import type { IssueResponse } from "@issue/shared"; import type { IssueResponse } from "@issue/shared";
import Avatar from "@/components/avatar"; import Avatar from "@/components/avatar";
import StatusTag from "@/components/status-tag";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -47,9 +48,7 @@ export function IssuesTable({
<TableCell> <TableCell>
<span className="flex items-center gap-2 max-w-full truncate"> <span className="flex items-center gap-2 max-w-full truncate">
{(columns.status == null || columns.status === true) && ( {(columns.status == null || columns.status === true) && (
<div className="text-xs px-1 bg-foreground/85 rounded text-background"> <StatusTag status={issueData.Issue.status} />
{issueData.Issue.status}
</div>
)} )}
{issueData.Issue.title} {issueData.Issue.title}
</span> </span>

View File

@@ -1,37 +1,41 @@
import type { ReactNode } from "react";
import { useState } from "react"; import { useState } from "react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import StatusTag from "./status-tag";
export function StatusSelect({ export function StatusSelect({
statuses, statuses,
value, value,
onChange, onChange,
placeholder = "Select status", placeholder = "Select status",
trigger,
}: { }: {
statuses: string[]; statuses: string[];
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
placeholder?: string; placeholder?: string;
trigger?: (args: { isOpen: boolean; value: string }) => ReactNode;
}) { }) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
return ( return (
<Select value={value} onValueChange={onChange} onOpenChange={setIsOpen}> <Select value={value} onValueChange={onChange} onOpenChange={setIsOpen}>
<SelectTrigger {trigger ? (
className="w-fit px-2 text-xs gap-1" trigger({ isOpen, value })
size="sm" ) : (
chevronClassName={"size-3 -mr-1"} <SelectTrigger
isOpen={isOpen} className="w-fit px-2 text-xs gap-1"
> size="sm"
<SelectValue placeholder={placeholder}>{value}</SelectValue> chevronClassName={"size-3 -mr-1"}
</SelectTrigger> isOpen={isOpen}
<SelectContent >
side="bottom" <SelectValue placeholder={placeholder}>{value}</SelectValue>
position="popper" </SelectTrigger>
align="start" )}
> <SelectContent side="bottom" position="popper" align="start">
{statuses.map((status) => ( {statuses.map((status) => (
<SelectItem key={status} value={status} textClassName="text-xs"> <SelectItem key={status} value={status} textClassName="text-xs">
{status} <StatusTag status={status} className="" />
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View File

@@ -0,0 +1,14 @@
import { cn } from "@/lib/utils";
export default function StatusTag({ status, className }: { status: string; className?: string }) {
return (
<div
className={cn(
"text-xs px-1 bg-foreground/85 rounded text-background inline-flex whitespace-nowrap",
className,
)}
>
{status}
</div>
);
}

View File

@@ -19,6 +19,7 @@ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.V
function SelectTrigger({ function SelectTrigger({
className, className,
size = "default", size = "default",
variant = "default",
children, children,
isOpen, isOpen,
label, label,
@@ -29,6 +30,7 @@ function SelectTrigger({
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & { }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
isOpen?: boolean; isOpen?: boolean;
size?: "sm" | "default"; size?: "sm" | "default";
variant?: "default" | "unstyled";
label?: string; label?: string;
hasValue?: boolean; hasValue?: boolean;
labelPosition?: "top" | "bottom"; labelPosition?: "top" | "bottom";
@@ -39,17 +41,21 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( className={cn(
"cursor-pointer border data-[placeholder]:text-muted-foreground", variant === "unstyled"
"[&_svg:not([class*='text-'])]:text-muted-foreground", ? "cursor-pointer bg-transparent shadow-none outline-none"
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", : [
"aria-invalid:border-destructive dark:hover:bg-muted/40", "cursor-pointer border data-[placeholder]:text-muted-foreground",
"relative flex w-fit items-center justify-between gap-2 border", "[&_svg:not([class*='text-'])]:text-muted-foreground",
"bg-transparent px-3 py-2 text-sm whitespace-nowrap", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
"shadow-xs outline-none disabled:cursor-not-allowed", "aria-invalid:border-destructive dark:hover:bg-muted/40",
"disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8", "relative flex w-fit items-center justify-between gap-2 border",
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex", "bg-transparent px-3 py-2 text-sm whitespace-nowrap",
"*:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2", "shadow-xs outline-none disabled:cursor-not-allowed",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8",
"*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex",
"*:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
],
className, className,
)} )}
{...props} {...props}