Update tooltip implementation and add TooltipContent and TooltipTrigger components

This commit is contained in:
2025-07-12 01:43:34 -03:00
parent 46de6b755a
commit 26c460f3de
4 changed files with 196 additions and 36 deletions

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@astrojs/mdx": "^4.3.0",
"@astrojs/react": "^4.3.0",
"@base-ui-components/react": "1.0.0-beta.1",
"@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.3",
"@tanstack/react-virtual": "^3.13.12",

84
web/pnpm-lock.yaml generated
View File

@@ -14,6 +14,9 @@ importers:
'@astrojs/react':
specifier: ^4.3.0
version: 4.3.0(@types/node@24.0.13)(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(jiti@2.4.2)(lightningcss@1.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@base-ui-components/react':
specifier: 1.0.0-beta.1
version: 1.0.0-beta.1(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot':
specifier: ^1.2.3
version: 1.2.3(@types/react@19.1.8)(react@19.1.0)
@@ -182,6 +185,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.27.6':
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@@ -194,6 +201,17 @@ packages:
resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==}
engines: {node: '>=6.9.0'}
'@base-ui-components/react@1.0.0-beta.1':
resolution: {integrity: sha512-7zmGiz4/+HKnv99lWftItoSMqnj2PdSvt2krh0/GP+Rj0xK0NMnFI/gIVvP7CB2G+k0JPUrRWXjXa3y08oiakg==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@capsizecss/unpack@2.4.0':
resolution: {integrity: sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q==}
@@ -356,6 +374,21 @@ packages:
cpu: [x64]
os: [win32]
'@floating-ui/core@1.7.2':
resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==}
'@floating-ui/dom@1.7.2':
resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==}
'@floating-ui/react-dom@2.1.4':
resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -1729,6 +1762,9 @@ packages:
remark-stringify@11.0.0:
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
restructure@3.0.2:
resolution: {integrity: sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==}
@@ -1814,6 +1850,9 @@ packages:
style-to-object@1.0.9:
resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
@@ -1991,6 +2030,11 @@ packages:
peerDependencies:
browserslist: '>= 4.21.0'
use-sync-external-store@1.5.0:
resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -2296,6 +2340,8 @@ snapshots:
'@babel/core': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/runtime@7.27.6': {}
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@@ -2319,6 +2365,19 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.27.1
'@base-ui-components/react@1.0.0-beta.1(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@babel/runtime': 7.27.6
'@floating-ui/react-dom': 2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@floating-ui/utils': 0.2.10
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
reselect: 5.1.1
tabbable: 6.2.0
use-sync-external-store: 1.5.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.8
'@capsizecss/unpack@2.4.0':
dependencies:
blob-to-buffer: 1.2.9
@@ -2410,6 +2469,23 @@ snapshots:
'@esbuild/win32-x64@0.25.6':
optional: true
'@floating-ui/core@1.7.2':
dependencies:
'@floating-ui/utils': 0.2.10
'@floating-ui/dom@1.7.2':
dependencies:
'@floating-ui/core': 1.7.2
'@floating-ui/utils': 0.2.10
'@floating-ui/react-dom@2.1.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@floating-ui/dom': 1.7.2
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@floating-ui/utils@0.2.10': {}
'@img/sharp-darwin-arm64@0.33.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
@@ -4190,6 +4266,8 @@ snapshots:
mdast-util-to-markdown: 2.1.2
unified: 11.0.5
reselect@5.1.1: {}
restructure@3.0.2: {}
retext-latin@4.0.0:
@@ -4335,6 +4413,8 @@ snapshots:
dependencies:
inline-style-parser: 0.2.4
tabbable@6.2.0: {}
tailwind-merge@3.3.1: {}
tailwindcss@4.1.11: {}
@@ -4474,6 +4554,10 @@ snapshots:
escalade: 3.2.0
picocolors: 1.1.1
use-sync-external-store@1.5.0(react@19.1.0):
dependencies:
react: 19.1.0
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3

View File

@@ -4,7 +4,7 @@ import type { VideoData } from '../types/video';
import { formatDuration, formatDate } from '../utils/csvParser';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Tooltip } from './ui/tooltip';
import { Tooltip,TooltipContent, TooltipTrigger } from './ui/tooltip';
interface VirtualTableProps {
data: VideoData[];
@@ -74,24 +74,32 @@ export function VirtualTable({ data }: VirtualTableProps) {
<div className="grid grid-cols-12 gap-3 px-4 py-3 items-center min-h-[80px]">
{/* Video Info */}
<div className="col-span-3 space-y-2 min-w-0">
<Tooltip content={video.video_title}>
<h3 className="font-medium text-sm leading-tight truncate">
<Tooltip>
<TooltipContent>
{video.video_title}
</TooltipContent>
<TooltipTrigger>
<h3 className="text-start font-medium text-sm leading-tight truncate sm:w-[150px] lg:w-[200px] xl:w-[250px] 2xl:w-[300px]">
{video.video_title}
</h3>
</TooltipTrigger>
</Tooltip>
<div className="flex flex-wrap gap-1">
{video.detailed_subtags.split(',').slice(0, 2).map((tag, i) => (
<Tooltip key={i} content={tag.trim()}>
<Badge variant="secondary" className="text-xs max-w-20 truncate">
{tag.trim()}
</Badge>
</Tooltip>
))}
{video.detailed_subtags.split(',').length > 2 && (
<Tooltip content={`+${video.detailed_subtags.split(',').length - 2} more tags`}>
<Tooltip>
<TooltipContent>
{video.detailed_subtags.split(',').slice(2).join(', ')}
</TooltipContent>
<TooltipTrigger>
<Badge variant="outline" className="text-xs">
+{video.detailed_subtags.split(',').length - 2}
</Badge>
</TooltipTrigger>
</Tooltip>
)}
</div>

View File

@@ -1,36 +1,103 @@
import * as React from "react"
import { Tooltip as BaseTooltip } from "@base-ui-components/react/tooltip"
import { cn } from "@/lib/utils"
interface TooltipProps {
content: string
children: React.ReactNode
className?: string
function TooltipProvider({
delay = 0,
closeDelay = 0,
...props
}: React.ComponentProps<typeof BaseTooltip.Provider>) {
return (
<BaseTooltip.Provider
data-slot="tooltip-provider"
delay={delay}
closeDelay={closeDelay}
{...props}
/>
)
}
export function Tooltip({ content, children, className }: TooltipProps) {
const [isVisible, setIsVisible] = React.useState(false)
function Tooltip({ ...props }: React.ComponentProps<typeof BaseTooltip.Root>) {
return (
<TooltipProvider>
<BaseTooltip.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
return (
<div className="relative inline-block">
<div
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
className="cursor-help"
>
{children}
</div>
{isVisible && (
<div className={cn(
"absolute z-50 px-3 py-2 text-sm text-white bg-gray-900 dark:bg-gray-700 rounded-lg shadow-lg",
"bottom-full left-1/2 transform -translate-x-1/2 mb-2",
"max-w-xs break-words",
"before:content-[''] before:absolute before:top-full before:left-1/2 before:transform before:-translate-x-1/2",
"before:border-4 before:border-transparent before:border-t-gray-900 dark:before:border-t-gray-700",
className
)}>
{content}
</div>
)}
</div>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof BaseTooltip.Trigger>) {
return <BaseTooltip.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipPortal({
...props
}: React.ComponentProps<typeof BaseTooltip.Portal>) {
return <BaseTooltip.Portal data-slot="tooltip-portal" {...props} />
}
function TooltipPositioner({
...props
}: React.ComponentProps<typeof BaseTooltip.Positioner>) {
return <BaseTooltip.Positioner data-slot="tooltip-positioner" {...props} />
}
function TooltipArrow({
...props
}: React.ComponentProps<typeof BaseTooltip.Arrow>) {
return <BaseTooltip.Arrow data-slot="tooltip-arrow" {...props} />
}
function TooltipContent({
className,
align = "center",
sideOffset = 8,
side = "top",
children,
...props
}: React.ComponentProps<typeof BaseTooltip.Popup> & {
align?: BaseTooltip.Positioner.Props["align"]
side?: BaseTooltip.Positioner.Props["side"]
sideOffset?: BaseTooltip.Positioner.Props["sideOffset"]
}) {
return (
<TooltipPortal>
<TooltipPositioner sideOffset={sideOffset} align={align} side={side}>
<BaseTooltip.Popup
data-slot="tooltip-content"
className={cn(
"bg-popover text-popover-foreground outline-border z-50 w-fit origin-[var(--transform-origin)] rounded-md px-3 py-1.5 text-xs text-balance shadow-sm outline -outline-offset-1 transition-[transform,scale,opacity] data-[ending-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:scale-95 data-[starting-style]:opacity-0",
className
)}
{...props}
>
{children}
<TooltipArrow className="data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180">
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V9H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="fill-popover"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="fill-border"
/>
</svg>
</TooltipArrow>
</BaseTooltip.Popup>
</TooltipPositioner>
</TooltipPortal>
)
}
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipPortal,
TooltipPositioner,
TooltipArrow,
TooltipProvider,
}