feature: Enhance PatternInfo with Tooltip and improve card layouts

Added shadcn Tooltip component for interactive pattern information. Wrapped all PatternInfo stat boxes and color swatches in tooltips with detailed metadata and explanations. Migrated PatternSummaryCard to use CardHeader/CardTitle/CardDescription for better semantic structure. Fixed Card component spacing issues across all cards.

Changes:
- Installed and added shadcn Tooltip component
- Added tooltips to Size, Stitches, and Colors stat boxes with explanatory text
- Wrapped color swatches in Tooltips with detailed thread information
- Added Separator between pattern stats and colors sections
- Migrated PatternSummaryCard to use CardHeader with semantic title/description
- Fixed Card gap-0 on all cards (FileUpload, PatternSummaryCard, ConnectionPrompt)
- Added explicit padding to PatternSummaryCard: CardHeader (p-4 pb-3) and CardContent (px-4 pt-0 pb-4)
- Updated components.json to use src/ paths instead of @/ aliases to fix shadcn install location

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik Bruhn 2025-12-20 19:52:16 +01:00
parent 2544504933
commit 054524cb5e
8 changed files with 266 additions and 65 deletions

View file

@ -11,11 +11,11 @@
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
"components": "src/components",
"utils": "src/lib/utils",
"ui": "src/components/ui",
"lib": "src/lib",
"hooks": "src/hooks"
},
"iconLibrary": "lucide"
}

76
package-lock.json generated
View file

@ -16,6 +16,7 @@
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.17",
"@types/web-bluetooth": "^0.0.21",
"class-variance-authority": "^0.7.1",
@ -4158,6 +4159,58 @@
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -4279,6 +4332,29 @@
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",

View file

@ -29,6 +29,7 @@
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.17",
"@types/web-bluetooth": "^0.0.21",
"class-variance-authority": "^0.7.1",

View file

@ -15,7 +15,7 @@ export function ConnectionPrompt() {
if (isBluetoothSupported()) {
return (
<Card className="p-0 border-l-4 border-gray-400 dark:border-gray-600">
<Card className="p-0 gap-0 border-l-4 border-gray-400 dark:border-gray-600">
<CardContent className="p-4 rounded-lg">
<div className="flex items-start gap-3 mb-3">
<div className="w-6 h-6 text-gray-600 dark:text-gray-400 flex-shrink-0 mt-0.5">

View file

@ -208,7 +208,7 @@ export function FileUpload() {
: "text-gray-600 dark:text-gray-400";
return (
<Card className={cn("p-0 border-l-4", borderColor)}>
<Card className={cn("p-0 gap-0 border-l-4", borderColor)}>
<CardContent className="p-4 rounded-lg">
<div className="flex items-start gap-3 mb-3">
<DocumentTextIcon

View file

@ -1,4 +1,11 @@
import type { PesPatternData } from "../formats/import/pesImporter";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Separator } from "@/components/ui/separator";
interface PatternInfoProps {
pesData: PesPatternData;
@ -11,50 +18,87 @@ export function PatternInfo({
}: PatternInfoProps) {
return (
<>
<div className="grid grid-cols-3 gap-2 text-xs mb-2">
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block">Size</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{" "}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</span>
</div>
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block">
Stitches
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{pesData.penStitches?.stitches.length.toLocaleString() ||
pesData.stitchCount.toLocaleString()}
{pesData.penStitches &&
pesData.penStitches.stitches.length !== pesData.stitchCount && (
<span
className="text-gray-500 dark:text-gray-500 font-normal ml-1"
title="Input stitch count from PES file (lock stitches were added for machine compatibility)"
>
({pesData.stitchCount.toLocaleString()})
<TooltipProvider>
<div className="grid grid-cols-3 gap-2 text-xs mb-2">
<Tooltip>
<TooltipTrigger asChild>
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
<span className="text-gray-600 dark:text-gray-400 block">Size</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{" "}
{((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm
</span>
)}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">Pattern dimensions (width × height)</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
<span className="text-gray-600 dark:text-gray-400 block">
Stitches
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{pesData.penStitches?.stitches.length.toLocaleString() ||
pesData.stitchCount.toLocaleString()}
{pesData.penStitches &&
pesData.penStitches.stitches.length !== pesData.stitchCount && (
<span
className="text-gray-500 dark:text-gray-500 font-normal ml-1"
title="Input stitch count from PES file (lock stitches were added for machine compatibility)"
>
({pesData.stitchCount.toLocaleString()})
</span>
)}
</span>
</div>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="text-xs">
{pesData.penStitches &&
pesData.penStitches.stitches.length !== pesData.stitchCount
? `Total stitches including lock stitches. Original file had ${pesData.stitchCount.toLocaleString()} stitches.`
: "Total number of stitches in the pattern"}
</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded cursor-help">
<span className="text-gray-600 dark:text-gray-400 block">
{showThreadBlocks ? "Colors / Blocks" : "Colors"}
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{showThreadBlocks
? `${pesData.uniqueColors.length} / ${pesData.threads.length}`
: pesData.uniqueColors.length}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
{showThreadBlocks
? `${pesData.uniqueColors.length} unique ${pesData.uniqueColors.length === 1 ? "color" : "colors"} across ${pesData.threads.length} thread ${pesData.threads.length === 1 ? "block" : "blocks"}`
: `${pesData.uniqueColors.length} unique ${pesData.uniqueColors.length === 1 ? "color" : "colors"} in the pattern`}
</p>
</TooltipContent>
</Tooltip>
</div>
<div className="bg-gray-200 dark:bg-gray-700/50 p-2 rounded">
<span className="text-gray-600 dark:text-gray-400 block">
{showThreadBlocks ? "Colors / Blocks" : "Colors"}
</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">
{showThreadBlocks
? `${pesData.uniqueColors.length} / ${pesData.threads.length}`
: pesData.uniqueColors.length}
</span>
</div>
</div>
</TooltipProvider>
<Separator className="mb-3" />
<div className="flex items-center gap-2 mb-2">
<span className="text-xs text-gray-600 dark:text-gray-400">
Colors:
</span>
<div className="flex gap-1">
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
<TooltipProvider>
<div className="flex gap-1">
{pesData.uniqueColors.slice(0, 8).map((color, idx) => {
// Primary metadata: brand and catalog number
const primaryMetadata = [
color.brand,
@ -85,20 +129,36 @@ export function PatternInfo({
: `Color ${idx + 1}: ${color.hex}\nUsed in thread blocks: ${threadNumbers}`;
return (
<div
key={idx}
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600"
style={{ backgroundColor: color.hex }}
title={tooltipText}
/>
<Tooltip key={idx}>
<TooltipTrigger asChild>
<div
className="w-3 h-3 rounded-full border border-gray-300 dark:border-gray-600 cursor-help"
style={{ backgroundColor: color.hex }}
/>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="text-xs whitespace-pre-line">{tooltipText}</p>
</TooltipContent>
</Tooltip>
);
})}
{pesData.uniqueColors.length > 8 && (
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none">
+{pesData.uniqueColors.length - 8}
</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600 border border-gray-400 dark:border-gray-500 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300 leading-none cursor-help">
+{pesData.uniqueColors.length - 8}
</div>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">
{pesData.uniqueColors.length - 8} more{" "}
{pesData.uniqueColors.length - 8 === 1 ? "color" : "colors"}
</p>
</TooltipContent>
</Tooltip>
)}
</div>
</div>
</TooltipProvider>
</div>
</>
);

View file

@ -5,7 +5,13 @@ import { canDeletePattern } from "../utils/machineStateHelpers";
import { PatternInfo } from "./PatternInfo";
import { DocumentTextIcon, TrashIcon } from "@heroicons/react/24/solid";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
import { Loader2 } from "lucide-react";
export function PatternSummaryCard() {
@ -30,23 +36,22 @@ export function PatternSummaryCard() {
const canDelete = canDeletePattern(machineStatus);
return (
<Card className="p-0 border-l-4 border-primary-600 dark:border-primary-500">
<CardContent className="p-4 rounded-lg">
<div className="flex items-start gap-3 mb-3">
<Card className="p-0 gap-0 border-l-4 border-primary-600 dark:border-primary-500">
<CardHeader className="p-4 pb-3">
<div className="flex items-start gap-3">
<DocumentTextIcon className="w-6 h-6 text-primary-600 dark:text-primary-400 flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-1">
Active Pattern
</h3>
<p
className="text-xs text-gray-600 dark:text-gray-400 truncate"
<CardTitle className="text-sm">Active Pattern</CardTitle>
<CardDescription
className="text-xs truncate"
title={currentFileName}
>
{currentFileName}
</p>
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="px-4 pt-0 pb-4">
<PatternInfo pesData={pesData} />
{canDelete && (

View file

@ -0,0 +1,59 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }