Files
image-studio/view/ToolOverlay.tsx
syntaxbullet 5915c62a9a refactor: Update bottom controls for improved styling and functionality
- Adjusted button styles across various components for consistency and better UX.
- Enhanced layout of action controls to utilize whitespace more effectively.
- Updated slider styles for a more modern appearance and improved usability.
- Refined input fields and labels for better accessibility and readability.
- Introduced new app surface styles for a cohesive design across the application.
- Added tests for canvas cursor behavior to ensure correct cursor display during operations.
2026-07-11 14:55:32 +02:00

94 lines
3.2 KiB
TypeScript

import { Cursor, Eraser, Hand, PaintBrush, DropHalf, MagicWand, Sparkle } from "@phosphor-icons/react";
import { commandIds } from "@commands/ids";
import type { AppStore } from "@editor/store";
import type { InteractionMode, OperationId, ToolId } from "@editor/tools";
import { availableOperationIds, availableToolIds } from "@editor/tools";
import { isOperationWorkspacePanel, type WorkspacePanel } from "@editor/state";
import { labelForTool } from "./toolLabels";
export type ToolOverlayProps = {
activeTool: ToolId;
interactionMode: InteractionMode;
panel: WorkspacePanel;
dispatch: AppStore["dispatch"];
};
export function ToolOverlay({ activeTool, interactionMode, panel, dispatch }: ToolOverlayProps) {
return (
<nav
aria-label="Tools"
className="app-surface pointer-events-auto flex w-11 flex-col items-center gap-1 rounded-xl p-1 text-white"
>
{availableToolIds.map((tool) => {
const active = !isOperationWorkspacePanel(panel) && isToolHighlighted(tool, activeTool, interactionMode);
const Icon = iconForTool(tool);
return (
<button
key={tool}
type="button"
aria-label={labelForTool(tool)}
aria-pressed={active}
title={labelForTool(tool)}
className={buttonClass(active)}
onClick={() => dispatch(commandIds.toolSetActive, { tool })}
>
<Icon size={18} weight={active ? "fill" : "regular"} />
</button>
);
})}
<div className="my-0.5 h-px w-6 bg-white/10" aria-hidden="true" />
{availableOperationIds.map((operation) => {
const active = panel === operation;
const Icon = iconForOperation(operation);
return (
<button
key={operation}
type="button"
aria-label={labelForOperation(operation)}
aria-pressed={active}
title={labelForOperation(operation)}
className={buttonClass(active)}
onClick={() => dispatch(commandIds.workspaceSetPanel, { panel: active ? "none" : operation })}
>
<Icon size={18} weight={active ? "fill" : "regular"} />
</button>
);
})}
</nav>
);
}
function iconForTool(tool: ToolId) {
switch (tool) {
case "brush":
return PaintBrush;
case "eraser":
return Eraser;
case "magicWand":
return MagicWand;
case "pan":
return Hand;
case "select":
return Cursor;
}
}
function iconForOperation(operation: OperationId) {
return operation === "generate" ? Sparkle : DropHalf;
}
function labelForOperation(operation: OperationId) {
return operation === "generate" ? "Generate" : "Chroma key";
}
function isToolHighlighted(tool: ToolId, activeTool: ToolId, interactionMode: InteractionMode) {
if (interactionMode.type === "temporary-pan") return tool === "pan";
return activeTool === tool;
}
function buttonClass(active: boolean) {
const base = "inline-flex size-9 items-center justify-center rounded-lg text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50";
return active ? `${base} bg-sky-300 text-slate-950` : `${base} text-white/55 hover:bg-white/[0.07] hover:text-white`;
}