minor visual adjustments to tags

This commit is contained in:
miloschwartz
2026-05-26 21:34:15 -07:00
parent 9eb55ba68c
commit 05e4ad3200
11 changed files with 263 additions and 151 deletions

View File

@@ -1165,6 +1165,7 @@
"labelsNotFound": "Labels not found",
"labelSearch": "Search labels",
"accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}",
"labelOverflowCount": "+{count, plural, one {# label} other {# labels}}",
"accessLabelFilterClear": "Clear label filters",
"selectColor": "Select color",
"createNewLabel": "Create new org label \"{label}\"",

View File

@@ -63,6 +63,7 @@ import { build } from "@server/build";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
@@ -740,34 +741,17 @@ function ClientResourceLabelCell({
});
}
const visibleLabels = optimisticLabels.slice(0, 3);
const overflowLabels = optimisticLabels.slice(3);
return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="p-1 size-auto rounded-full"
className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")}
>
<span className="sr-only">{t("addLabels")}</span>
@@ -782,6 +766,18 @@ function ClientResourceLabelCell({
/>
</PopoverContent>
</Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div>
);
}

View File

@@ -19,10 +19,14 @@ import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilter
import { CheckIcon, Funnel } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { Badge } from "./ui/badge";
import { orgQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import { useDebounce } from "use-debounce";
import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LABEL_COLORS } from "./labels-selector";
const MAX_VISIBLE_SUMMARY_LABELS = 3;
type LabelColumnFilterButtonProps = {
selectedValues: string[];
@@ -58,33 +62,47 @@ export function LabelColumnFilterButton({
[selectedValues]
);
const selectedLabels = useMemo(
() =>
selectedValues.map((name) => {
const foundLabel = labels.find((label) => label.name === name);
return {
name,
color: foundLabel?.color ?? LABEL_COLORS.gray
};
}),
[selectedValues, labels]
);
const summary = useMemo(() => {
if (selectedValues.length === 0) {
if (selectedLabels.length === 0) {
return null;
}
if (selectedValues.length === 1) {
const foundLabel = labels.find((o) => o.name === selectedValues[0]);
if (foundLabel) {
return (
<div className="inline-flex items-center gap-1">
<div
className="size-3 rounded-full bg-(--color) flex-none"
style={{
// @ts-expect-error css color
"--color": foundLabel.color
}}
/>
{foundLabel.name}
</div>
);
}
return selectedValues[0];
}
return t("accessLabelFilterCount", {
count: selectedValues.length
});
}, [selectedValues, labels, t]);
const visibleLabels = selectedLabels.slice(0, MAX_VISIBLE_SUMMARY_LABELS);
const overflowLabels = selectedLabels.slice(MAX_VISIBLE_SUMMARY_LABELS);
return (
<div className="flex min-w-0 flex-nowrap items-center gap-1">
{visibleLabels.map((label) => (
<LabelBadge
key={label.name}
displayOnly
name={label.name}
color={label.color}
className="shrink-0"
/>
))}
{overflowLabels.length > 0 && (
<LabelOverflowBadge
labels={overflowLabels}
displayOnly
className="shrink-0"
/>
)}
</div>
);
}, [selectedLabels]);
function toggle(value: string) {
const next = selectedSet.has(value)
@@ -94,7 +112,7 @@ export function LabelColumnFilterButton({
}
return (
<div className="flex items-center justify-end">
<div className="flex items-center">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
@@ -111,18 +129,7 @@ export function LabelColumnFilterButton({
<div className="flex items-center gap-2 min-w-0">
<span className="shrink-0">{label}</span>
<Funnel className="size-4 flex-none shrink-0" />
{summary && (
<Badge
className={cn(
"truncate max-w-40",
selectedValues.length === 1 &&
"pl-1.5 pr-2 h-auto"
)}
variant="secondary"
>
{summary}
</Badge>
)}
{summary}
</div>
</Button>
</PopoverTrigger>
@@ -168,7 +175,7 @@ export function LabelColumnFilterButton({
)}
/>
<div
className="size-4 rounded-full bg-(--color) flex-none"
className="size-2 rounded-full bg-(--color) flex-none"
style={{
// @ts-expect-error css color
"--color": label.color

View File

@@ -41,6 +41,7 @@ import { useDebouncedCallback } from "use-debounce";
import z from "zod";
import { ColumnFilterButton } from "./ColumnFilterButton";
import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { Badge } from "./ui/badge";
import { ControlledDataTable } from "./ui/controlled-data-table";
@@ -657,34 +658,17 @@ function MachineClientLabelCell({
});
}
const visibleLabels = optimisticLabels.slice(0, 3);
const overflowLabels = optimisticLabels.slice(3);
return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="p-1 size-auto rounded-full"
className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")}
>
<span className="sr-only">{t("addLabels")}</span>
@@ -699,6 +683,18 @@ function MachineClientLabelCell({
/>
</PopoverContent>
</Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div>
);
}

View File

@@ -72,7 +72,6 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
<FormControl>
<Input
{...field}
placeholder={t("labelPlaceholder")}
/>
</FormControl>
<FormMessage />
@@ -104,13 +103,16 @@ export function OrgLabelForm({ onSubmit, defaultValue }: OrgLabelFormProps) {
className="flex items-center gap-2"
>
<div
className="size-4 rounded-full bg-(--color) flex-none"
className="size-2 rounded-full bg-(--color) flex-none"
style={{
// @ts-expect-error css color
"--color": value
}}
/>
<span data-name>{color}</span>
<span data-name>
{color.charAt(0).toUpperCase() +
color.slice(1)}
</span>
</SelectItem>
)
)}

View File

@@ -121,11 +121,9 @@ export default function OrgLabelsTable({
)
},
{
accessorKey: "actions",
id: "actions",
enableHiding: false,
header: () => {
return <span className="p-3">{t("actions")}</span>;
},
header: () => <span className="p-3"></span>,
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -234,6 +232,7 @@ export default function OrgLabelsTable({
onRefresh={refreshData}
isRefreshing={isRefreshing || isFiltering}
rowCount={rowCount}
stickyRightColumn="actions"
/>
</>
);

View File

@@ -72,6 +72,7 @@ import { ControlledDataTable } from "./ui/controlled-data-table";
import UptimeMiniBar from "./UptimeMiniBar";
import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
export type TargetHealth = {
@@ -808,34 +809,17 @@ function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) {
});
}
const visibleLabels = optimisticLabels.slice(0, 3);
const overflowLabels = optimisticLabels.slice(3);
return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="p-1 size-auto rounded-full"
className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")}
>
<span className="sr-only">{t("addLabels")}</span>
@@ -850,6 +834,18 @@ function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) {
/>
</PopoverContent>
</Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div>
);
}

View File

@@ -62,6 +62,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { cn } from "@app/lib/cn";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { LabelBadge } from "./label-badge";
import { LabelOverflowBadge } from "./label-overflow-badge";
import { LabelsSelector, type SelectedLabel } from "./labels-selector";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
@@ -395,7 +396,7 @@ export default function SitesTable({
variant="ghost"
size="sm"
onClick={() => setResourcesDialogSite(siteRow)}
className="flex h-8 items-center gap-2 px-2 font-normal"
className="flex h-8 items-center gap-2 px-0 font-normal"
>
<span className="text-sm tabular-nums">
{siteRow.resourceCount} {t("resources")}
@@ -735,34 +736,17 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) {
});
}
const visibleLabels = optimisticLabels.slice(0, 3);
const overflowLabels = optimisticLabels.slice(3);
return (
<div className="inline-flex flex-wrap items-center justify-end w-full gap-1">
{optimisticLabels.slice(0, 3).map((label) => (
<LabelBadge
key={label.labelId}
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
{optimisticLabels.length > 3 && (
<Button
variant="outline"
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"px-1.5 py-0 h-auto"
)}
onClick={() => setIsPopoverOpen(true)}
>
+{optimisticLabels.length - 3}
</Button>
)}
<div className="inline-flex w-full min-w-0 flex-nowrap items-center gap-1 overflow-hidden">
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button
size="icon"
variant="outline"
className="p-1 size-auto rounded-full"
className="size-auto shrink-0 rounded-full p-1"
title={t("addLabels")}
>
<span className="sr-only">{t("addLabels")}</span>
@@ -777,6 +761,18 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) {
/>
</PopoverContent>
</Popover>
{visibleLabels.map((label) => (
<LabelBadge
key={label.labelId}
className="shrink-0"
onClick={() => setIsPopoverOpen(true)}
{...label}
/>
))}
<LabelOverflowBadge
labels={overflowLabels}
onClick={() => setIsPopoverOpen(true)}
/>
</div>
);
}

View File

@@ -1,40 +1,62 @@
import { cn } from "@app/lib/cn";
import { Button } from "./ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
const labelBadgeClassName =
"inline-flex h-auto items-center gap-1 rounded-full border border-input bg-background py-0 pl-1.5 pr-2 text-sm shadow-xs";
export type LabelBadgeProps = {
name: string;
color: string;
onClick?: () => void;
className?: string;
displayOnly?: boolean;
};
export function LabelBadge({
onClick,
name,
color,
className
className,
displayOnly = false
}: LabelBadgeProps) {
return (
<Button
variant="outline"
onClick={onClick}
className={cn(
"inline-flex gap-1 items-center",
"rounded-full text-sm cursor-pointer",
"pl-1.5 pr-2 py-0 h-auto",
className
)}
>
const content = (
<>
<div
className="size-3 rounded-full bg-(--color) flex-none"
className="size-2 flex-none rounded-full bg-(--color)"
style={{
// @ts-expect-error css color
"--color": color
}}
/>
<span className="whitespace-nowrap text-ellipsis max-w-16 overflow-hidden relative">
<span className="relative max-w-24 overflow-hidden text-ellipsis whitespace-nowrap">
{name}
</span>
</Button>
</>
);
return (
<Tooltip>
<TooltipTrigger asChild>
{displayOnly ? (
<span className={cn(labelBadgeClassName, className)}>
{content}
</span>
) : (
<Button
variant="outline"
onClick={onClick}
className={cn(
labelBadgeClassName,
"cursor-pointer",
className
)}
>
{content}
</Button>
)}
</TooltipTrigger>
<TooltipContent>{name}</TooltipContent>
</Tooltip>
);
}

View File

@@ -0,0 +1,97 @@
import { cn } from "@app/lib/cn";
import { useTranslations } from "next-intl";
import { Button } from "./ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export type LabelOverflowItem = {
color: string;
name?: string;
};
const labelOverflowBadgeClassName =
"inline-flex h-auto shrink-0 items-center gap-1.5 rounded-full border border-input bg-background py-0 pl-1.5 pr-2 text-sm shadow-xs";
export type LabelOverflowBadgeProps = {
labels: LabelOverflowItem[];
onClick?: () => void;
className?: string;
displayOnly?: boolean;
};
const MAX_OVERFLOW_COLORS = 3;
export function LabelOverflowBadge({
labels,
onClick,
className,
displayOnly = false
}: LabelOverflowBadgeProps) {
const t = useTranslations();
if (labels.length === 0) {
return null;
}
const displayColors = labels
.slice(0, MAX_OVERFLOW_COLORS)
.map((label) => label.color);
const overflowNames = labels
.map((label) => label.name)
.filter((name): name is string => Boolean(name));
const tooltipContent =
overflowNames.length > 0
? overflowNames.join(", ")
: t("labelOverflowCount", { count: labels.length });
const content = (
<>
<span className="inline-flex items-center">
{displayColors.map((color, index) => (
<span
key={index}
className={cn(
"size-2 flex-none rounded-full bg-(--color) ring-1 ring-background",
index > 0 && "-ml-1"
)}
style={{
// @ts-expect-error css color
"--color": color
}}
/>
))}
</span>
<span className="whitespace-nowrap text-muted-foreground">
{t("labelOverflowCount", { count: labels.length })}
</span>
</>
);
return (
<Tooltip>
<TooltipTrigger asChild>
{displayOnly ? (
<span
className={cn(labelOverflowBadgeClassName, className)}
>
{content}
</span>
) : (
<Button
variant="outline"
onClick={onClick}
className={cn(
labelOverflowBadgeClassName,
"cursor-pointer",
className
)}
>
{content}
</Button>
)}
</TooltipTrigger>
<TooltipContent>{tooltipContent}</TooltipContent>
</Tooltip>
);
}

View File

@@ -215,9 +215,9 @@ export function LabelsSelector({
aria-hidden
tabIndex={-1}
/>
<div className="min-w-0 flex-1 flex items-center gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<span
className="inline-block size-3 flex-none rounded-full bg-(--label-color)"
className="inline-block size-2 flex-none rounded-full bg-(--label-color)"
style={{
// @ts-expect-error CSS variable
"--label-color": label.color