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-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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Group name="grid" listening={false}>
|
||||
{lines.verticalLines.map((points, i) => (
|
||||
<Line
|
||||
key={`v-${i}`}
|
||||
points={points}
|
||||
stroke={gridColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
))}
|
||||
{lines.horizontalLines.map((points, i) => (
|
||||
<Line
|
||||
key={`h-${i}`}
|
||||
points={points}
|
||||
stroke={gridColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Group name="grid" listening={false}>
|
||||
{lines.verticalLines.map((points, i) => (
|
||||
<Line
|
||||
key={`v-${i}`}
|
||||
points={points}
|
||||
stroke={gridColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
))}
|
||||
{lines.horizontalLines.map((points, i) => (
|
||||
<Line
|
||||
key={`h-${i}`}
|
||||
points={points}
|
||||
stroke={gridColor}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Grid.displayName = "Grid";
|
||||
|
||||
export const Origin = memo(() => {
|
||||
const originColor = canvasColors.origin();
|
||||
export const Origin = memo(({ colorOverride }: { colorOverride?: string }) => {
|
||||
const originColor = colorOverride ?? canvasColors.origin();
|
||||
|
||||
return (
|
||||
<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 {
|
||||
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() {
|
|||
<CardHeader className="p-4 pb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<PhotoIcon className={`w-6 h-6 ${iconColor} flex-shrink-0 mt-0.5`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<CardTitle className="text-sm">Pattern Preview</CardTitle>
|
||||
{hasPattern ? (
|
||||
<CardDescription className="text-xs">
|
||||
{patternDimensions}
|
||||
</CardDescription>
|
||||
) : (
|
||||
<CardDescription className="text-xs">
|
||||
No pattern loaded
|
||||
</CardDescription>
|
||||
)}
|
||||
<div className="flex-1 min-w-0 flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-sm">Pattern Preview</CardTitle>
|
||||
{hasPattern ? (
|
||||
<CardDescription className="text-xs">
|
||||
{patternDimensions}
|
||||
</CardDescription>
|
||||
) : (
|
||||
<CardDescription className="text-xs">
|
||||
No pattern loaded
|
||||
</CardDescription>
|
||||
)}
|
||||
</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>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pt-0 pb-4 flex-1 flex flex-col min-h-0">
|
||||
<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}
|
||||
>
|
||||
{containerSize.width > 0 && (
|
||||
|
|
@ -184,8 +206,9 @@ export function PatternCanvas() {
|
|||
gridSize={100}
|
||||
bounds={displayPattern.bounds}
|
||||
machineInfo={machineInfo}
|
||||
colorOverride={canvasGridColor}
|
||||
/>
|
||||
<Origin />
|
||||
<Origin colorOverride={canvasOriginColor} />
|
||||
{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