From 7817835f16f3b3dd70214388b276c619e21869d7 Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Thu, 26 Mar 2026 12:42:57 +0100 Subject: [PATCH] feature: Add light/dark background toggle to pattern preview Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 45 ++++++++ package.json | 1 + .../PatternCanvas/KonvaComponents.tsx | 103 +++++++++--------- .../PatternCanvas/PatternCanvas.tsx | 53 ++++++--- src/components/ui/switch.tsx | 27 +++++ 5 files changed, 164 insertions(+), 65 deletions(-) create mode 100644 src/components/ui/switch.tsx diff --git a/package-lock.json b/package-lock.json index 8a10324..bb67607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.17", "@types/web-bluetooth": "^0.0.21", @@ -4433,6 +4434,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "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-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "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": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -4570,6 +4600,21 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "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-rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", diff --git a/package.json b/package.json index e97c6ac..90a7680 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.17", "@types/web-bluetooth": "^0.0.21", diff --git a/src/components/PatternCanvas/KonvaComponents.tsx b/src/components/PatternCanvas/KonvaComponents.tsx index 2d4c27b..c0def57 100644 --- a/src/components/PatternCanvas/KonvaComponents.tsx +++ b/src/components/PatternCanvas/KonvaComponents.tsx @@ -10,67 +10,70 @@ interface GridProps { gridSize: number; bounds: { minX: number; maxX: number; minY: number; maxY: number }; machineInfo: MachineInfo | null; + colorOverride?: string; } -export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => { - const lines = useMemo(() => { - const gridMinX = machineInfo ? -machineInfo.maxWidth / 2 : bounds.minX; - const gridMaxX = machineInfo ? machineInfo.maxWidth / 2 : bounds.maxX; - const gridMinY = machineInfo ? -machineInfo.maxHeight / 2 : bounds.minY; - const gridMaxY = machineInfo ? machineInfo.maxHeight / 2 : bounds.maxY; +export const Grid = memo( + ({ gridSize, bounds, machineInfo, colorOverride }: GridProps) => { + const lines = useMemo(() => { + const gridMinX = machineInfo ? -machineInfo.maxWidth / 2 : bounds.minX; + const gridMaxX = machineInfo ? machineInfo.maxWidth / 2 : bounds.maxX; + const gridMinY = machineInfo ? -machineInfo.maxHeight / 2 : bounds.minY; + const gridMaxY = machineInfo ? machineInfo.maxHeight / 2 : bounds.maxY; - const verticalLines: number[][] = []; - const horizontalLines: number[][] = []; + const verticalLines: number[][] = []; + const horizontalLines: number[][] = []; - // Vertical lines - for ( - let x = Math.floor(gridMinX / gridSize) * gridSize; - x <= gridMaxX; - x += gridSize - ) { - verticalLines.push([x, gridMinY, x, gridMaxY]); - } + // Vertical lines + for ( + let x = Math.floor(gridMinX / gridSize) * gridSize; + x <= gridMaxX; + x += gridSize + ) { + verticalLines.push([x, gridMinY, x, gridMaxY]); + } - // Horizontal lines - for ( - let y = Math.floor(gridMinY / gridSize) * gridSize; - y <= gridMaxY; - y += gridSize - ) { - horizontalLines.push([gridMinX, y, gridMaxX, y]); - } + // Horizontal lines + for ( + let y = Math.floor(gridMinY / gridSize) * gridSize; + y <= gridMaxY; + y += gridSize + ) { + horizontalLines.push([gridMinX, y, gridMaxX, y]); + } - return { verticalLines, horizontalLines }; - }, [gridSize, bounds, machineInfo]); + return { verticalLines, horizontalLines }; + }, [gridSize, bounds, machineInfo]); - const gridColor = canvasColors.grid(); + const gridColor = colorOverride ?? canvasColors.grid(); - return ( - - {lines.verticalLines.map((points, i) => ( - - ))} - {lines.horizontalLines.map((points, i) => ( - - ))} - - ); -}); + return ( + + {lines.verticalLines.map((points, i) => ( + + ))} + {lines.horizontalLines.map((points, i) => ( + + ))} + + ); + }, +); Grid.displayName = "Grid"; -export const Origin = memo(() => { - const originColor = canvasColors.origin(); +export const Origin = memo(({ colorOverride }: { colorOverride?: string }) => { + const originColor = colorOverride ?? canvasColors.origin(); return ( diff --git a/src/components/PatternCanvas/PatternCanvas.tsx b/src/components/PatternCanvas/PatternCanvas.tsx index 498c941..1a913cb 100644 --- a/src/components/PatternCanvas/PatternCanvas.tsx +++ b/src/components/PatternCanvas/PatternCanvas.tsx @@ -1,4 +1,4 @@ -import { useRef, useMemo } from "react"; +import { useRef, useMemo, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { useMachineStore, @@ -8,7 +8,7 @@ import { useMachineUploadStore } from "../../stores/useMachineUploadStore"; import { usePatternStore } from "../../stores/usePatternStore"; import { Stage, Layer } from "react-konva"; import Konva from "konva"; -import { PhotoIcon } from "@heroicons/react/24/solid"; +import { PhotoIcon, SunIcon, MoonIcon } from "@heroicons/react/24/solid"; import { Grid, Origin, Hoop } from "./KonvaComponents"; import { Card, @@ -21,6 +21,7 @@ import { ThreadLegend } from "./ThreadLegend"; import { PatternPositionIndicator } from "./PatternPositionIndicator"; import { ZoomControls } from "./ZoomControls"; import { PatternLayer } from "./PatternLayer"; +import { Switch } from "@/components/ui/switch"; import { useCanvasViewport, usePatternTransform } from "@/hooks"; export function PatternCanvas() { @@ -103,6 +104,16 @@ export function PatternCanvas() { isUploading, }); + const [previewDark, setPreviewDark] = useState( + () => window.matchMedia("(prefers-color-scheme: dark)").matches, + ); + + const canvasBg = previewDark + ? "bg-gray-900 border-gray-600" + : "bg-gray-200 border-gray-300"; + const canvasGridColor = previewDark ? "#404040" : "#e0e0e0"; + const canvasOriginColor = previewDark ? "#999999" : "#888888"; + const hasPattern = pesData || uploadedPesData; const borderColor = hasPattern ? "border-tertiary-600 dark:border-tertiary-500" @@ -138,23 +149,34 @@ export function PatternCanvas() {
-
- Pattern Preview - {hasPattern ? ( - - {patternDimensions} - - ) : ( - - No pattern loaded - - )} +
+
+ Pattern Preview + {hasPattern ? ( + + {patternDimensions} + + ) : ( + + No pattern loaded + + )} +
+
+ + + +
{containerSize.width > 0 && ( @@ -184,8 +206,9 @@ export function PatternCanvas() { gridSize={100} bounds={displayPattern.bounds} machineInfo={machineInfo} + colorOverride={canvasGridColor} /> - + {machineInfo && } )} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..6338184 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch };