- 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.
126 lines
6.0 KiB
TypeScript
126 lines
6.0 KiB
TypeScript
import { useEffect, useState } from "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 { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
|
|
import { handleDeleteSelectionKey, handleHistoryKey, keybindEventFromKeyboardEvent } from "@input/index";
|
|
import { useAppState } from "./useAppState";
|
|
import { downloadArtboardPng } from "./exportArtboardPng";
|
|
import { useImageImport } from "./useImageImport";
|
|
import { useViewportActivityIsland } from "./useViewportActivityIsland";
|
|
import "./index.css";
|
|
|
|
export type AppProps = {
|
|
app: ImageStudioApp;
|
|
};
|
|
|
|
export function App({ app }: AppProps) {
|
|
const state = useAppState(app.store);
|
|
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];
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
const target = event.target;
|
|
const editableTarget =
|
|
target instanceof HTMLElement &&
|
|
(target.isContentEditable || target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement);
|
|
|
|
if (editableTarget) return;
|
|
|
|
const historyConsumed = handleHistoryKey({ event: keybindEventFromKeyboardEvent(event), dispatch: app.store.dispatch });
|
|
if (historyConsumed) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (event.altKey || event.ctrlKey || event.metaKey) return;
|
|
|
|
if (event.key === "Escape" && app.store.getState().editor.maskEdit) {
|
|
app.store.dispatch(commandIds.toolExitMaskEdit, undefined);
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (event.key.toLowerCase() === "l") {
|
|
setLayersOpen((open) => !open);
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
const consumed = handleDeleteSelectionKey({
|
|
event: keybindEventFromKeyboardEvent(event),
|
|
selection: app.store.getState().editor.selection,
|
|
dispatch: app.store.dispatch,
|
|
});
|
|
if (consumed) event.preventDefault();
|
|
};
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [app.store]);
|
|
const transformBounds = transformTarget ? resolveTransformTargetBounds(state.document, transformTarget) : undefined;
|
|
|
|
return (
|
|
<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-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={24} />
|
|
</button>
|
|
<button type="button" className={topBarButtonClass()} disabled={!activeArtboard} onClick={() => activeArtboard && void downloadArtboardPng(activeArtboard, state.document.assets)}>
|
|
<DownloadSimple size={24} />
|
|
</button>
|
|
<button type="button" className={topBarButtonClass(layersOpen)} aria-pressed={layersOpen} onClick={() => setLayersOpen((open) => !open)}>
|
|
<Stack size={24} weight={layersOpen ? "fill" : "regular"} />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
<div className="absolute left-3 top-1/2 z-10 -translate-y-1/2">
|
|
<ToolOverlay
|
|
activeTool={state.editor.tools.activeTool}
|
|
interactionMode={state.editor.tools.interactionMode}
|
|
dispatch={app.store.dispatch}
|
|
/>
|
|
</div>
|
|
<LayersSheet
|
|
document={state.document}
|
|
selection={state.editor.selection}
|
|
maskEdit={state.editor.maskEdit}
|
|
open={layersOpen}
|
|
dispatch={app.store.dispatch}
|
|
/>
|
|
<div className="absolute inset-x-0 bottom-4 z-10 flex justify-center">
|
|
<BottomControlsIsland
|
|
viewport={state.editor.viewport}
|
|
visible={state.editor.tools.activeTool === "brush" || state.editor.tools.activeTool === "eraser" || Boolean(transformBounds) || viewportActivityIsland.visible}
|
|
action={viewportActivityIsland.action}
|
|
activeTool={state.editor.tools.activeTool}
|
|
brushSettings={state.editor.tools.brush}
|
|
editingMask={Boolean(state.editor.maskEdit)}
|
|
maskViewMode={state.editor.maskEdit?.viewMode ?? "composite"}
|
|
transformBounds={viewportActivityIsland.visible ? undefined : transformBounds}
|
|
transformTarget={viewportActivityIsland.visible ? undefined : transformTarget}
|
|
dispatch={app.store.dispatch}
|
|
/>
|
|
</div>
|
|
<CanvasViewport store={app.store} />
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function topBarButtonClass(active = false) {
|
|
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`;
|
|
}
|
|
|
|
export default App;
|