mirror of
https://github.com/jhbruhn/respira.git
synced 2026-04-27 17:45:45 +00:00
feature: Add light/dark background toggle to pattern preview
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3e30909311
commit
7817835f16
5 changed files with 164 additions and 65 deletions
45
package-lock.json
generated
45
package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/web-bluetooth": "^0.0.21",
|
"@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": {
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@types/web-bluetooth": "^0.0.21",
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ interface GridProps {
|
||||||
gridSize: number;
|
gridSize: number;
|
||||||
bounds: { minX: number; maxX: number; minY: number; maxY: number };
|
bounds: { minX: number; maxX: number; minY: number; maxY: number };
|
||||||
machineInfo: MachineInfo | null;
|
machineInfo: MachineInfo | null;
|
||||||
|
colorOverride?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
|
export const Grid = memo(
|
||||||
|
({ gridSize, bounds, machineInfo, colorOverride }: GridProps) => {
|
||||||
const lines = useMemo(() => {
|
const lines = useMemo(() => {
|
||||||
const gridMinX = machineInfo ? -machineInfo.maxWidth / 2 : bounds.minX;
|
const gridMinX = machineInfo ? -machineInfo.maxWidth / 2 : bounds.minX;
|
||||||
const gridMaxX = machineInfo ? machineInfo.maxWidth / 2 : bounds.maxX;
|
const gridMaxX = machineInfo ? machineInfo.maxWidth / 2 : bounds.maxX;
|
||||||
|
|
@ -43,7 +45,7 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
|
||||||
return { verticalLines, horizontalLines };
|
return { verticalLines, horizontalLines };
|
||||||
}, [gridSize, bounds, machineInfo]);
|
}, [gridSize, bounds, machineInfo]);
|
||||||
|
|
||||||
const gridColor = canvasColors.grid();
|
const gridColor = colorOverride ?? canvasColors.grid();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="grid" listening={false}>
|
<Group name="grid" listening={false}>
|
||||||
|
|
@ -65,12 +67,13 @@ export const Grid = memo(({ gridSize, bounds, machineInfo }: GridProps) => {
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Grid.displayName = "Grid";
|
Grid.displayName = "Grid";
|
||||||
|
|
||||||
export const Origin = memo(() => {
|
export const Origin = memo(({ colorOverride }: { colorOverride?: string }) => {
|
||||||
const originColor = canvasColors.origin();
|
const originColor = colorOverride ?? canvasColors.origin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group name="origin" listening={false}>
|
<Group name="origin" listening={false}>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRef, useMemo } from "react";
|
import { useRef, useMemo, useState } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import {
|
import {
|
||||||
useMachineStore,
|
useMachineStore,
|
||||||
|
|
@ -8,7 +8,7 @@ import { useMachineUploadStore } from "../../stores/useMachineUploadStore";
|
||||||
import { usePatternStore } from "../../stores/usePatternStore";
|
import { usePatternStore } from "../../stores/usePatternStore";
|
||||||
import { Stage, Layer } from "react-konva";
|
import { Stage, Layer } from "react-konva";
|
||||||
import Konva from "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 { Grid, Origin, Hoop } from "./KonvaComponents";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -21,6 +21,7 @@ import { ThreadLegend } from "./ThreadLegend";
|
||||||
import { PatternPositionIndicator } from "./PatternPositionIndicator";
|
import { PatternPositionIndicator } from "./PatternPositionIndicator";
|
||||||
import { ZoomControls } from "./ZoomControls";
|
import { ZoomControls } from "./ZoomControls";
|
||||||
import { PatternLayer } from "./PatternLayer";
|
import { PatternLayer } from "./PatternLayer";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { useCanvasViewport, usePatternTransform } from "@/hooks";
|
import { useCanvasViewport, usePatternTransform } from "@/hooks";
|
||||||
|
|
||||||
export function PatternCanvas() {
|
export function PatternCanvas() {
|
||||||
|
|
@ -103,6 +104,16 @@ export function PatternCanvas() {
|
||||||
isUploading,
|
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 hasPattern = pesData || uploadedPesData;
|
||||||
const borderColor = hasPattern
|
const borderColor = hasPattern
|
||||||
? "border-tertiary-600 dark:border-tertiary-500"
|
? "border-tertiary-600 dark:border-tertiary-500"
|
||||||
|
|
@ -138,7 +149,8 @@ export function PatternCanvas() {
|
||||||
<CardHeader className="p-4 pb-3">
|
<CardHeader className="p-4 pb-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<PhotoIcon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
|
<PhotoIcon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<CardTitle className="text-sm">Pattern Preview</CardTitle>
|
<CardTitle className="text-sm">Pattern Preview</CardTitle>
|
||||||
{hasPattern ? (
|
{hasPattern ? (
|
||||||
<CardDescription className="text-xs">
|
<CardDescription className="text-xs">
|
||||||
|
|
@ -150,11 +162,21 @@ export function PatternCanvas() {
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<SunIcon className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
||||||
|
<Switch
|
||||||
|
checked={previewDark}
|
||||||
|
onCheckedChange={setPreviewDark}
|
||||||
|
aria-label="Toggle preview background"
|
||||||
|
/>
|
||||||
|
<MoonIcon className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col min-h-0">
|
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col min-h-0">
|
||||||
<div
|
<div
|
||||||
className="relative w-full flex-1 min-h-0 border border-gray-300 dark:border-gray-600 rounded bg-gray-200 dark:bg-gray-900 overflow-hidden"
|
className={`relative w-full flex-1 min-h-0 border rounded overflow-hidden ${canvasBg}`}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
{containerSize.width > 0 && (
|
{containerSize.width > 0 && (
|
||||||
|
|
@ -184,8 +206,9 @@ export function PatternCanvas() {
|
||||||
gridSize={100}
|
gridSize={100}
|
||||||
bounds={displayPattern.bounds}
|
bounds={displayPattern.bounds}
|
||||||
machineInfo={machineInfo}
|
machineInfo={machineInfo}
|
||||||
|
colorOverride={canvasGridColor}
|
||||||
/>
|
/>
|
||||||
<Origin />
|
<Origin colorOverride={canvasOriginColor} />
|
||||||
{machineInfo && <Hoop machineInfo={machineInfo} />}
|
{machineInfo && <Hoop machineInfo={machineInfo} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
27
src/components/ui/switch.tsx
Normal file
27
src/components/ui/switch.tsx
Normal file
|
|
@ -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<typeof SwitchPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SwitchPrimitives.Root
|
||||||
|
className={cn(
|
||||||
|
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<SwitchPrimitives.Thumb
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwitchPrimitives.Root>
|
||||||
|
));
|
||||||
|
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||||
|
|
||||||
|
export { Switch };
|
||||||
Loading…
Reference in a new issue