From 054524cb5e72102e34a0c74c164112effdc2796c Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sat, 20 Dec 2025 19:52:16 +0100 Subject: [PATCH] feature: Enhance PatternInfo with Tooltip and improve card layouts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components.json | 10 +- package-lock.json | 76 +++++++++++++ package.json | 1 + src/components/ConnectionPrompt.tsx | 2 +- src/components/FileUpload.tsx | 2 +- src/components/PatternInfo.tsx | 154 ++++++++++++++++++-------- src/components/PatternSummaryCard.tsx | 27 +++-- src/components/ui/tooltip.tsx | 59 ++++++++++ 8 files changed, 266 insertions(+), 65 deletions(-) create mode 100644 src/components/ui/tooltip.tsx diff --git a/components.json b/components.json index 589babc..2147b0e 100644 --- a/components.json +++ b/components.json @@ -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" } diff --git a/package-lock.json b/package-lock.json index 4059650..e96cb0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 94f52f2..b8f8607 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/ConnectionPrompt.tsx b/src/components/ConnectionPrompt.tsx index 4a64423..00a9d78 100644 --- a/src/components/ConnectionPrompt.tsx +++ b/src/components/ConnectionPrompt.tsx @@ -15,7 +15,7 @@ export function ConnectionPrompt() { if (isBluetoothSupported()) { return ( - +
diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 15309ca..e0004b8 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -208,7 +208,7 @@ export function FileUpload() { : "text-gray-600 dark:text-gray-400"; return ( - +
-
-
- Size - - {((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{" "} - {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm - -
-
- - Stitches - - - {pesData.penStitches?.stitches.length.toLocaleString() || - pesData.stitchCount.toLocaleString()} - {pesData.penStitches && - pesData.penStitches.stitches.length !== pesData.stitchCount && ( - - ({pesData.stitchCount.toLocaleString()}) + +
+ + +
+ Size + + {((pesData.bounds.maxX - pesData.bounds.minX) / 10).toFixed(1)} x{" "} + {((pesData.bounds.maxY - pesData.bounds.minY) / 10).toFixed(1)} mm - )} - +
+
+ +

Pattern dimensions (width × height)

+
+
+ + + +
+ + Stitches + + + {pesData.penStitches?.stitches.length.toLocaleString() || + pesData.stitchCount.toLocaleString()} + {pesData.penStitches && + pesData.penStitches.stitches.length !== pesData.stitchCount && ( + + ({pesData.stitchCount.toLocaleString()}) + + )} + +
+
+ +

+ {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"} +

+
+
+ + + +
+ + {showThreadBlocks ? "Colors / Blocks" : "Colors"} + + + {showThreadBlocks + ? `${pesData.uniqueColors.length} / ${pesData.threads.length}` + : pesData.uniqueColors.length} + +
+
+ +

+ {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`} +

+
+
-
- - {showThreadBlocks ? "Colors / Blocks" : "Colors"} - - - {showThreadBlocks - ? `${pesData.uniqueColors.length} / ${pesData.threads.length}` - : pesData.uniqueColors.length} - -
-
+ + +
Colors: -
- {pesData.uniqueColors.slice(0, 8).map((color, idx) => { + +
+ {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 ( -
+ + +
+ + +

{tooltipText}

+
+ ); })} {pesData.uniqueColors.length > 8 && ( -
- +{pesData.uniqueColors.length - 8} -
+ + +
+ +{pesData.uniqueColors.length - 8} +
+
+ +

+ {pesData.uniqueColors.length - 8} more{" "} + {pesData.uniqueColors.length - 8 === 1 ? "color" : "colors"} +

+
+
)} -
+
+
); diff --git a/src/components/PatternSummaryCard.tsx b/src/components/PatternSummaryCard.tsx index 354c66e..71874c5 100644 --- a/src/components/PatternSummaryCard.tsx +++ b/src/components/PatternSummaryCard.tsx @@ -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 ( - - -
+ + +
-

- Active Pattern -

-

Active Pattern + {currentFileName} -

+
- +
+ {canDelete && ( diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..715bf76 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -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) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }