feat: render automatic inbox label explanations with themed tooltips

This commit is contained in:
Matthew Meszaros
2026-09-17 05:02:58 -07:00
parent 99273114b7
commit deeef293e4
5 changed files with 76 additions and 64 deletions
@@ -25,11 +25,11 @@ import {
StatStrip,
} from "@/components/layout/Page";
import { NoAccess } from "@/components/layout/NoAccess";
import { TagMeaningTooltip } from "@/components/ui/tag-meaning-tooltip";
import { usePermission } from "@/hooks/usePermission";
import useInboxTagReview from "@/lib/api/hooks/app/inboxtag/useInboxTagReview";
import type { InboxTagRow } from "@/lib/api/models/app/inboxtag/InboxTagReview";
import { cn } from "@/lib/utils";
import { tagMeaning } from "@/lib/unibox/tagMeanings";
const PRIORITY_TONE: Record<string, string> = {
now: "bg-rose-50 text-rose-700",
@@ -83,13 +83,11 @@ function Row({ r }: { r: InboxTagRow }) {
<span className="text-[11.5px] text-slate-400">no labels</span>
) : (
r.labels.map((l) => (
<span
key={l}
title={tagMeaning(l) || l}
className="px-1.5 rounded bg-slate-100 text-slate-700 text-[11px] cursor-help"
>
{l}
</span>
<TagMeaningTooltip key={l} title={l}>
<span className="px-1.5 rounded bg-slate-100 text-slate-700 text-[11px] cursor-help">
{l}
</span>
</TagMeaningTooltip>
))
)}
</div>
@@ -25,7 +25,7 @@ import useFlipPlacement from "@/hooks/useFlipPlacement";
import clippedTitle from "@/lib/helper/clippedTitle";
import useCreateCategory from "@/lib/api/hooks/app/categories/useCreateCategory";
import type Category from "@/lib/api/models/app/Category";
import { tagMeaning } from "@/lib/unibox/tagMeanings";
import { TagMeaningTooltip } from "@/components/ui/tag-meaning-tooltip";
interface Props {
// Selected ids — kept as ids so the consumer can store them in the
@@ -225,46 +225,41 @@ export function CategoryChip({
onRemove?: () => void;
compact?: boolean;
}) {
// Automatic labels carry their meaning on hover. A chip reading "going-cold"
// is only useful to somebody who already knows the taxonomy, and nobody
// does on the first day. A label the workspace made itself needs no gloss,
// so it gets the plain title.
const meaning = tagMeaning(category.title);
return (
<span
title={meaning || category.title}
className={`inline-flex items-center gap-1 min-w-0 ${compact ? "h-4 pl-1 pr-1 text-[10px]" : "h-5 pl-1.5 pr-1 text-[11px]"} rounded font-medium`}
style={{
backgroundColor: hexToRgba(category.color, 0.12),
color: category.color,
border: `1px solid ${hexToRgba(category.color, 0.25)}`,
}}
>
<TagMeaningTooltip title={category.title}>
<span
className={`${compact ? "size-1.5" : "size-2"} rounded-full shrink-0`}
style={{ backgroundColor: category.color }}
/>
<span
className="truncate min-w-0 max-w-[72px] md:max-w-none"
{...clippedTitle}
className={`inline-flex items-center gap-1 min-w-0 ${compact ? "h-4 pl-1 pr-1 text-[10px]" : "h-5 pl-1.5 pr-1 text-[11px]"} rounded font-medium`}
style={{
backgroundColor: hexToRgba(category.color, 0.12),
color: category.color,
border: `1px solid ${hexToRgba(category.color, 0.25)}`,
}}
>
{category.title}
</span>
{onRemove && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
className="opacity-70 hover:opacity-100"
aria-label={`Remove ${category.title}`}
<span
className={`${compact ? "size-1.5" : "size-2"} rounded-full shrink-0`}
style={{ backgroundColor: category.color }}
/>
<span
className="truncate min-w-0 max-w-[72px] md:max-w-none"
{...clippedTitle}
>
<XIcon className="w-2.5 h-2.5" />
</button>
)}
</span>
{category.title}
</span>
{onRemove && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
className="opacity-70 hover:opacity-100"
aria-label={`Remove ${category.title}`}
>
<XIcon className="w-2.5 h-2.5" />
</button>
)}
</span>
</TagMeaningTooltip>
);
}
+16 -15
View File
@@ -47,7 +47,8 @@ import {
PopoverMenuTrigger,
} from "@/components/ui/popover-menu";
import type { UniboxFolder } from "@/lib/api/models/app/unibox/UniboxSearch";
import { tagMeaning } from "@/lib/unibox/tagMeanings";
import { TagMeaningTooltip } from "@/components/ui/tag-meaning-tooltip";
import { isAutomaticTag } from "@/lib/unibox/tagMeanings";
export type UniboxScope =
| { kind: "all" }
@@ -246,16 +247,17 @@ export function ScopeRail({ scope, onChange }: ScopeRailProps) {
searchPlaceholder="Filter labels"
getSearchKey={(c) => c.title}
renderItem={(c) => (
<Item
key={c.id}
icon={<Dot color={c.color} />}
label={c.title}
title={tagMeaning(c.title) || undefined}
count={c.unread || c.total || undefined}
accent={c.unread > 0}
active={active === `category:${c.id}`}
onClick={() => onChange({ kind: "category", categoryId: c.id })}
/>
<TagMeaningTooltip key={c.id} title={c.title}>
<Item
icon={<Dot color={c.color} />}
label={c.title}
hideNativeTitle={isAutomaticTag(c.title)}
count={c.unread || c.total || undefined}
accent={c.unread > 0}
active={active === `category:${c.id}`}
onClick={() => onChange({ kind: "category", categoryId: c.id })}
/>
</TagMeaningTooltip>
)}
/>
)}
@@ -547,7 +549,7 @@ function FolderItem({
function Item({
icon,
label,
title,
hideNativeTitle,
count,
accent,
active,
@@ -555,8 +557,7 @@ function Item({
}: {
icon: React.ReactNode;
label: string;
/** Hover explanation, for the automatic labels whose names are not self-evident. */
title?: string;
hideNativeTitle?: boolean;
count?: number;
accent?: boolean;
active?: boolean;
@@ -567,7 +568,7 @@ function Item({
type="button"
onClick={onClick}
className={cn("group/item", ROW, active ? ROW_ACTIVE : ROW_IDLE)}
title={title || label}
title={hideNativeTitle ? undefined : label}
>
<span className={cn("shrink-0", active ? "text-slate-800" : "text-slate-400 group-hover/item:text-slate-600")}>
{icon}
@@ -19,7 +19,7 @@ import { useUserProfile } from "@/hooks/context/user";
import useCreateCategory from "@/lib/api/hooks/app/categories/useCreateCategory";
import useThreadLabels from "@/lib/api/hooks/app/unibox/useThreadLabels";
import useSetThreadLabels from "@/lib/api/hooks/app/unibox/useSetThreadLabels";
import { tagMeaning } from "@/lib/unibox/tagMeanings";
import { TagMeaningTooltip } from "@/components/ui/tag-meaning-tooltip";
interface Props {
threadId: string;
@@ -180,9 +180,9 @@ export function ThreadLabelMenu({ threadId, open, onOpenChange }: Props) {
className="size-2.5 rounded-full shrink-0"
style={{ backgroundColor: c.color }}
/>
<span className="truncate" title={tagMeaning(c.title) || undefined}>
{c.title}
</span>
<TagMeaningTooltip title={c.title}>
<span className="truncate">{c.title}</span>
</TagMeaningTooltip>
{checked && (
<span className="ml-auto text-[10px] text-slate-300">assigned</span>
)}
@@ -0,0 +1,18 @@
import type { ReactElement } from "react";
import { tagMeaning } from "@/lib/unibox/tagMeanings";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export function TagMeaningTooltip({ title, children }: { title: string; children: ReactElement }) {
const meaning = tagMeaning(title);
if (!meaning) return children;
return (
<Tooltip>
<TooltipTrigger asChild>{children}</TooltipTrigger>
<TooltipContent sideOffset={6} className="max-w-72">
{meaning}
</TooltipContent>
</Tooltip>
);
}