feat: enhance bottom controls and UI components

- Updated BottomControlsIsland for improved layout and styling.
- Refactored LayersSheet to enhance usability and visual consistency.
- Modified ToolOverlay for better tool selection experience.
- Improved BrushControls with new ColorPicker and Slider components.
- Enhanced PanControls and TransformControls for better user interaction.
- Updated ZoomControls for consistent button sizes and styles.
- Introduced new styles for bottom controls in styles.ts and index.css.
- Added BottomControlColorPicker, BottomControlSelectMenu, and BottomControlSlider components for better modularity and reusability.
This commit is contained in:
syntaxbullet
2026-07-03 22:28:34 +02:00
parent 4590b47e19
commit e8073e6146
16 changed files with 409 additions and 155 deletions

View File

@@ -1,12 +1,11 @@
import { useEffect, useState } from "react";
import { DownloadSimple, FolderOpen, ImageSquare, Stack } from "@phosphor-icons/react";
import { DownloadSimple, FolderOpen, Stack } from "@phosphor-icons/react";
import type { ImageStudioApp } from "@app/app";
import { commandIds } from "@commands/ids";
import { BottomControlsIsland } from "./BottomControlsIsland";
import { CanvasViewport } from "./CanvasViewport";
import { LayersSheet } from "./LayersSheet";
import { ToolOverlay } from "./ToolOverlay";
import { labelForTool } from "./toolLabels";
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
import { handleDeleteSelectionKey, handleHistoryKey, keybindEventFromKeyboardEvent } from "@input/index";
import { useAppState } from "./useAppState";
@@ -21,13 +20,11 @@ export type AppProps = {
export function App({ app }: AppProps) {
const state = useAppState(app.store);
const zoomPercent = Math.round(state.editor.viewport.zoom * 100);
const viewportActivityIsland = useViewportActivityIsland(state.editor.viewport);
const imageImport = useImageImport(app.store);
const [layersOpen, setLayersOpen] = useState(false);
const transformTarget = state.editor.transformSession?.target ?? selectedTransformTarget(state.document, state.editor.selection);
const activeArtboard = state.document.artboards.find((artboard) => artboard.id === state.editor.selection.artboardId) ?? state.document.artboards[0];
const activeToolLabel = state.editor.maskEdit ? "Mask edit" : labelForTool(state.editor.tools.activeTool);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -53,7 +50,7 @@ export function App({ app }: AppProps) {
}
if (event.key.toLowerCase() === "l") {
setLayersOpen(true);
setLayersOpen((open) => !open);
event.preventDefault();
return;
}
@@ -72,40 +69,18 @@ export function App({ app }: AppProps) {
const transformBounds = transformTarget ? resolveTransformTargetBounds(state.document, transformTarget) : undefined;
return (
<main className="relative h-full overflow-hidden bg-background text-foreground">
<main className="relative h-full overflow-hidden bg-[radial-gradient(circle_at_20%_18%,rgba(148,163,184,0.18),transparent_34%),radial-gradient(circle_at_82%_22%,rgba(71,85,105,0.22),transparent_36%),radial-gradient(circle_at_48%_88%,rgba(30,41,59,0.28),transparent_40%),linear-gradient(135deg,#020617_0%,#0f172a_46%,#111827_100%)] text-foreground">
{imageImport.input}
<header className="pointer-events-none absolute inset-x-3 top-3 z-10 flex h-12 items-center justify-between gap-3 rounded-2xl border border-white/10 bg-zinc-950/85 px-2.5 text-white shadow-2xl shadow-black/35 backdrop-blur-xl">
<div className="flex min-w-0 items-center gap-3 pl-1.5">
<div className="grid size-7 place-items-center rounded-lg bg-white text-black shadow-sm">
<ImageSquare size={18} weight="fill" />
</div>
<div className="min-w-0">
<h1 className="truncate text-sm font-semibold leading-4 tracking-wide">Image Studio</h1>
<div className="truncate text-[11px] leading-4 text-white/45">{state.document.name}</div>
</div>
</div>
<div className="hidden min-w-0 flex-1 items-center justify-center gap-1.5 md:flex">
<span className={topBarChipClass()}>{activeToolLabel}</span>
<span className={topBarChipClass()}>{zoomPercent}%</span>
<span className={topBarChipClass()}>
{state.editor.viewport.size.w}×{state.editor.viewport.size.h}
</span>
{activeArtboard ? <span className="truncate rounded-full bg-sky-400/15 px-3 py-1 text-xs font-medium text-sky-100 ring-1 ring-sky-300/20">{activeArtboard.name}</span> : null}
</div>
<div className="pointer-events-auto flex items-center gap-1.5">
<header className="pointer-events-none absolute inset-x-3 top-3 z-10 flex h-20 items-center justify-end gap-4 rounded-full px-4 text-white backdrop-blur-xl">
<div className="pointer-events-auto flex items-center gap-2">
<button type="button" className={topBarButtonClass()} onClick={imageImport.openFilePicker}>
<FolderOpen size={16} />
<span className="hidden sm:inline">Import</span>
<FolderOpen size={24} />
</button>
<button type="button" className={topBarButtonClass()} disabled={!activeArtboard} onClick={() => activeArtboard && void downloadArtboardPng(activeArtboard, state.document.assets)}>
<DownloadSimple size={16} />
<span className="hidden sm:inline">Export</span>
<DownloadSimple size={24} />
</button>
<button type="button" className={topBarButtonClass(layersOpen)} aria-pressed={layersOpen} onClick={() => setLayersOpen((open) => !open)}>
<Stack size={16} weight={layersOpen ? "fill" : "regular"} />
<span className="hidden sm:inline">Layers</span>
<Stack size={24} weight={layersOpen ? "fill" : "regular"} />
</button>
</div>
</header>
@@ -142,12 +117,8 @@ export function App({ app }: AppProps) {
);
}
function topBarChipClass() {
return "rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-xs font-medium text-white/70";
}
function topBarButtonClass(active = false) {
const base = "inline-flex h-8 items-center gap-1.5 rounded-lg px-3 text-xs font-medium transition disabled:pointer-events-none disabled:opacity-35 focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30";
const base = "inline-flex size-12 items-center justify-center rounded-full text-sm font-medium transition disabled:pointer-events-none disabled:opacity-35 focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30";
return active ? `${base} bg-white text-black hover:bg-white hover:text-black` : `${base} text-white/75 hover:bg-white/10 hover:text-white`;
}