feat(editor): Items category bar as icon-grid with tooltips (#349)

Render the function-tree category roots as a Build-style square-tile grid
(icon when available, otherwise a two-letter abbreviation) with a hover
tooltip for the full name, so the Items and Build panels read the same.

Share one TooltipProvider with disableHoverableContent + the icon-grid
tooltip class to avoid the flicker when moving between adjacent tiles.
Legacy fallback panel category bar wraps instead of scrolling horizontally.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-05-29 14:29:46 -04:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 986d75026f
commit f51cea9a85
2 changed files with 50 additions and 35 deletions
@@ -1,10 +1,16 @@
'use client' 'use client'
import type { AssetInput } from '@pascal-app/core' import type { AssetInput } from '@pascal-app/core'
import { Root as TooltipRoot } from '@radix-ui/react-tooltip'
import NextImage from 'next/image' import NextImage from 'next/image'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { cn } from '../../../../../lib/utils' import { cn } from '../../../../../lib/utils'
import { ItemCatalog } from '../../../item-catalog/item-catalog' import { ItemCatalog } from '../../../item-catalog/item-catalog'
import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '../../../../../components/ui/primitives/tooltip'
/** A function-axis taxonomy node, assembled into a tree by the embedder. */ /** A function-axis taxonomy node, assembled into a tree by the embedder. */
export type FunctionTreeNode = { export type FunctionTreeNode = {
@@ -109,40 +115,49 @@ export function FunctionTreePanel({
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
{/* Root nodes as category tabs */} {/* Root nodes as a category grid — icon when available, otherwise a
<div className="flex shrink-0 gap-1 overflow-x-auto border-border/70 border-b p-2"> two-letter abbreviation, with the full name in a hover tooltip.
Mirrors the Build tab's tile grid so the two panels read the same. */}
<TooltipProvider delayDuration={0} disableHoverableContent>
<div className="grid shrink-0 grid-cols-5 gap-1.5 border-border/70 border-b p-2">
{functionTree.map((root) => { {functionTree.map((root) => {
const isActive = activeRoot?.slug === root.slug const isActive = activeRoot?.slug === root.slug
return ( return (
<TooltipRoot key={root.slug}>
<TooltipTrigger asChild>
<button <button
className={cn( className={cn(
'flex shrink-0 flex-col items-center gap-1 rounded-xl px-3 py-2 transition-colors', 'relative flex aspect-square items-center justify-center rounded-xl transition-all duration-200',
isActive isActive
? 'bg-sidebar-accent text-sidebar-accent-foreground' ? 'bg-primary/10 ring-1 ring-primary/50'
: 'text-muted-foreground hover:bg-sidebar-accent/50 hover:text-foreground', : 'bg-muted/40 opacity-70 grayscale hover:bg-muted hover:opacity-100 hover:grayscale-0',
)} )}
key={root.slug}
onClick={() => selectRoot(root.slug)} onClick={() => selectRoot(root.slug)}
type="button" type="button"
> >
{root.iconUrl ? ( {root.iconUrl ? (
<NextImage <NextImage
alt={root.name} alt={root.name}
className={cn('size-7 object-contain', !isActive && 'opacity-60 grayscale')} className="size-7 object-contain"
height={28} height={28}
src={root.iconUrl} src={root.iconUrl}
width={28} width={28}
/> />
) : ( ) : (
<div className="flex size-7 items-center justify-center rounded-lg bg-muted/50 font-semibold text-[11px] uppercase"> <span className="font-semibold text-muted-foreground text-xs uppercase">
{root.name.slice(0, 2)} {root.name.slice(0, 2)}
</div> </span>
)} )}
<span className="font-medium text-[10px] leading-none">{root.name}</span>
</button> </button>
</TooltipTrigger>
<TooltipContent className="icon-grid-tooltip pointer-events-none" side="top">
{root.name}
</TooltipContent>
</TooltipRoot>
) )
})} })}
</div> </div>
</TooltipProvider>
{/* Search + source filter */} {/* Search + source filter */}
<div className="flex shrink-0 flex-col gap-2 border-border/70 border-b p-2"> <div className="flex shrink-0 flex-col gap-2 border-border/70 border-b p-2">
@@ -173,7 +173,7 @@ function LegacyItemsPanel({
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
{/* Category tabs */} {/* Category tabs */}
<div className="flex shrink-0 gap-1 overflow-x-auto border-border/70 border-b p-2"> <div className="flex shrink-0 flex-wrap gap-1 border-border/70 border-b p-2">
{furnishTools.map((cat) => { {furnishTools.map((cat) => {
const isActive = activeCategory.catalogCategory === cat.catalogCategory const isActive = activeCategory.catalogCategory === cat.catalogCategory
return ( return (