Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/app/editor/tool-rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@roprgm/ui/tooltip";
import type { ReactNode } from "react";
import { GithubIcon } from "@/components/icons/github";
import { InfoIcon } from "@/components/icons/info";
import { PaintColors } from "@/features/paint/colors";
import { RailPaintColors } from "@/features/paint/colors";
import { type Tool, tools, useTool } from "./tools";

/** Opens in a new tab, leaving the photo open. */
Expand Down Expand Up @@ -51,10 +51,11 @@ export function ToolTabList({
}) {
const editing = tools.filter((entry) => entry.group === "edit");
return (
// The tools fill the rail, 6px from the window's edge and 7px from the border.
<Tabs
value={selected}
onValueChange={onSelect}
className="flex w-11 shrink-0 flex-col gap-1 overflow-auto border-edge border-r material-panel p-1.5"
className="flex w-12 shrink-0 flex-col gap-1 overflow-auto border-edge border-r material-panel py-1.5 pr-1.75 pl-1.5"
>
<TabList aria-label="Tools" className="flex-col">
{editing.map((entry) => (
Expand All @@ -65,6 +66,7 @@ export function ToolTabList({
value={entry}
disabled={!onSelect}
size="icon-lg"
className="size-8.5"
aria-label={entry.label}
/>
}
Expand Down Expand Up @@ -98,7 +100,7 @@ export function ToolRail() {
const { tool, setTool } = useTool();
return (
<ToolTabList selected={tool} onSelect={setTool}>
<PaintColors vertical />
<RailPaintColors />
</ToolTabList>
);
}
2 changes: 1 addition & 1 deletion src/components/icons/eyedropper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function EyedropperIcon(props: IconProps) {
return (
<Icon viewBox="0 0 20 20" {...props}>
<g transform="rotate(45 10 10)">
<path d="M7.75 6.25V4a2.25 2.25 0 0 1 4.5 0v2.25M6.25 6.25h7.5M8.5 6.25V13l1.5 3.5 1.5-3.5V6.25M8.5 11h3" />
<path d="M7.41 6.69V4.11a2.59 2.59 0 0 1 5.17 0v2.59M5.69 6.69h8.62M8.28 6.69V14.46l1.72 4.02 1.72-4.02V6.69M8.28 12.16h3.45" />
</g>
</Icon>
);
Expand Down
9 changes: 9 additions & 0 deletions src/components/icons/swap-corner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Icon, type IconProps } from "./icon";

export function SwapCornerIcon(props: IconProps) {
return (
<Icon viewBox="0 0 12 12" strokeWidth="1.25" {...props}>
<path d="M.625 2.625H3A6.375 6.375 0 0 1 9.375 9v2.375M2.625.625l-2 2 2 2m4.75 4.75 2 2 2-2" />
</Icon>
);
}
7 changes: 4 additions & 3 deletions src/components/ui/color-swatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { cn } from "cn";
import { type ComponentProps, useEffect, useRef } from "react";

/**
* A raised square of a color that opens the browser's color picker. The picker streams changes while
* it moves, and `onClose` follows the last of them.
* A flat square of a color that opens the browser's color picker: a control's shadow without the lit
* top edge, which would tint the color, and a faint edge that parts it from a background of its
* color. The picker streams changes while it moves, and `onClose` follows the last of them.
*/
export function ColorSwatch({
label,
Expand Down Expand Up @@ -31,7 +32,7 @@ export function ColorSwatch({
<span
{...props}
className={cn(
"relative block size-5 shrink-0 rounded-sm material-control focus-ring",
"relative block size-5 shrink-0 rounded-sm shadow-card inset-ring inset-ring-foreground/10 focus-ring",
className,
)}
style={{ backgroundColor: value }}
Expand Down
124 changes: 98 additions & 26 deletions src/features/paint/colors.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { IconButton } from "@roprgm/ui/icon-button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@roprgm/ui/tooltip";
import { cn } from "cn";
import type { ReactNode } from "react";
import { defaultColors, useBrushTool } from "@/components/editor/brush-tool";
import { EyedropperIcon } from "@/components/icons/eyedropper";
import { SwapIcon } from "@/components/icons/swap";
import { SwapCornerIcon } from "@/components/icons/swap-corner";
import { ColorSwatch } from "@/components/ui/color-swatch";
import { hexColor } from "@/lib/parse";

Expand Down Expand Up @@ -54,7 +56,7 @@ function Swatch({
<ColorSwatch
label={label}
value={value}
className={cn("absolute size-4.5", className)}
className={cn("absolute", className)}
onChange={onChange}
/>
}
Expand All @@ -64,35 +66,56 @@ function Swatch({
);
}

/**
* Two raised squares, as in Photoshop: the primary over the secondary, with swap and pick after them,
* in a row or, in the tool rail, a column.
*/
export function PaintColors({ vertical = false }: { vertical?: boolean }) {
/** Two squares, as in Photoshop: the primary over the secondary, with `children` in a free corner. */
function Swatches({
colors,
side,
className,
swatch,
children,
}: {
colors: ReturnType<typeof usePaintColors>;
side: Side;
className: string;
swatch: string;
children?: ReactNode;
}) {
return (
<div className={cn("relative shrink-0", className)}>
<Swatch
label="Secondary color"
value={colors.secondary}
side={side}
className={cn("right-0 bottom-0", swatch)}
onChange={colors.setSecondary}
/>
<Swatch
label="Primary color"
value={colors.primary}
side={side}
className={cn("top-0 left-0", swatch)}
onChange={colors.setPrimary}
/>
{children}
</div>
);
}

/** The colors in a row, with swap and pick after them. */
export function PaintColors() {
const colors = usePaintColors();
const side = vertical ? "right" : "top";
return (
<div className={cn("flex items-center gap-0.5", vertical && "flex-col")}>
<div className="relative m-1 size-7 shrink-0">
<Swatch
label="Secondary color"
value={colors.secondary}
side={side}
className="right-0 bottom-0"
onChange={colors.setSecondary}
/>
<Swatch
label="Primary color"
value={colors.primary}
side={side}
className="top-0 left-0"
onChange={colors.setPrimary}
/>
</div>
<div className="flex items-center gap-0.5">
<Swatches
colors={colors}
side="top"
className="m-1 size-7"
swatch="size-4.5"
/>
<IconButton
label="Swap colors"
shortcut="X"
side={side}
side="top"
size="icon-sm"
className="rounded-full"
onClick={colors.swap}
Expand All @@ -103,7 +126,7 @@ export function PaintColors({ vertical = false }: { vertical?: boolean }) {
<IconButton
label="Pick a color"
shortcut="I"
side={side}
side="top"
size="icon-sm"
className="rounded-full"
onClick={colors.pick}
Expand All @@ -114,3 +137,52 @@ export function PaintColors({ vertical = false }: { vertical?: boolean }) {
</div>
);
}

/**
* The colors in the tool rail: pick as one more tool, then the squares with swap in their free
* corner, as in Photoshop. X swaps from the keyboard, so the arrow takes no focus.
*/
export function RailPaintColors() {
const colors = usePaintColors();
return (
<>
{colors.pick && (
<IconButton
label="Pick a color"
shortcut="I"
side="right"
size="icon-lg"
className="size-8.5"
onClick={colors.pick}
>
<EyedropperIcon className="size-5" />
</IconButton>
)}
<Swatches
colors={colors}
side="right"
className="mt-2 size-8.5"
swatch="size-5.25"
>
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
tabIndex={-1}
aria-label="Swap colors"
className="absolute -top-px right-0 flex size-3.25 cursor-pointer items-start justify-end text-secondary transition-colors hover:text-foreground"
onClick={colors.swap}
/>
}
>
<SwapCornerIcon className="size-2.75" />
</TooltipTrigger>
<TooltipContent side="right" shortcut="X">
Swap colors
</TooltipContent>
</Tooltip>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</Swatches>
</>
);
}
Loading