feat: add command palette functionality and shortcuts
- Implemented command palette with keyboard shortcut (⌘K) for opening. - Added commands for opening, closing, setting query, and selecting items in the command palette. - Created tests for command palette commands and input handling. - Enhanced layer actions with functions for adding, grouping, and deleting layers. - Updated UI components to integrate command palette and shortcuts.
This commit is contained in:
49
view/App.tsx
49
view/App.tsx
@@ -5,6 +5,7 @@ import { commandIds } from "@commands/ids";
|
||||
import { BottomControlsIsland } from "./BottomControlsIsland";
|
||||
import { brushUnavailableHint } from "./canvas/brush";
|
||||
import { CanvasViewport } from "./CanvasViewport";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
import { GenerateSheet } from "./GenerateSheet";
|
||||
import { LayersSheet } from "./LayersSheet";
|
||||
import { ShortcutsDisplay } from "./ShortcutsDisplay";
|
||||
@@ -12,7 +13,7 @@ import { ToolOverlay } from "./ToolOverlay";
|
||||
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
|
||||
import type { AppState } from "@editor/state";
|
||||
import type { ToolId } from "@editor/tools";
|
||||
import { handleDeleteSelectionKey, handleHistoryKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index";
|
||||
import { handleCommandPaletteKey, handleDeleteSelectionKey, handleHistoryKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index";
|
||||
import { shallowEqual, useAppState } from "./useAppState";
|
||||
import { downloadArtboardPng } from "./exportArtboardPng";
|
||||
import { useImageImport } from "./useImageImport";
|
||||
@@ -25,7 +26,7 @@ export type AppProps = {
|
||||
|
||||
export function App({ app }: AppProps) {
|
||||
const shellState = useAppState(app.store, selectAppShellState, shallowEqual);
|
||||
const { document, selection, viewport, tools, generation, transformSession, maskEdit } = shellState;
|
||||
const { document, selection, viewport, tools, generation, commandPalette, transformSession, maskEdit } = shellState;
|
||||
const viewportActivityIsland = useViewportActivityIsland(viewport);
|
||||
const imageImport = useImageImport(app.store);
|
||||
const [layersOpen, setLayersOpen] = useState(false);
|
||||
@@ -65,6 +66,15 @@ export function App({ app }: AppProps) {
|
||||
else openGenerate();
|
||||
}, [closeGenerate, generateOpen, openGenerate]);
|
||||
|
||||
const openLayers = useCallback(() => {
|
||||
closeGenerate();
|
||||
setLayersOpen(true);
|
||||
}, [closeGenerate]);
|
||||
|
||||
const closeLayers = useCallback(() => {
|
||||
setLayersOpen(false);
|
||||
}, []);
|
||||
|
||||
const toggleLayers = useCallback(() => {
|
||||
const nextOpen = !layersOpen;
|
||||
if (nextOpen) closeGenerate();
|
||||
@@ -73,6 +83,21 @@ export function App({ app }: AppProps) {
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const keybindEvent = keybindEventFromKeyboardEvent(event);
|
||||
const paletteConsumed = handleCommandPaletteKey({ event: keybindEvent, dispatch: app.store.dispatch });
|
||||
if (paletteConsumed) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (app.store.getState().editor.commandPalette.open) {
|
||||
if (event.key === "Escape") {
|
||||
app.store.dispatch(commandIds.commandPaletteClose, undefined);
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
const editableTarget =
|
||||
target instanceof HTMLElement &&
|
||||
@@ -80,7 +105,7 @@ export function App({ app }: AppProps) {
|
||||
|
||||
if (editableTarget) return;
|
||||
|
||||
const historyConsumed = handleHistoryKey({ event: keybindEventFromKeyboardEvent(event), dispatch: app.store.dispatch });
|
||||
const historyConsumed = handleHistoryKey({ event: keybindEvent, dispatch: app.store.dispatch });
|
||||
if (historyConsumed) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
@@ -95,7 +120,6 @@ export function App({ app }: AppProps) {
|
||||
}
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
const keybindEvent = keybindEventFromKeyboardEvent(event);
|
||||
|
||||
const toolConsumed = handleToolKey({ event: keybindEvent, dispatch: app.store.dispatch });
|
||||
if (toolConsumed) {
|
||||
@@ -126,6 +150,21 @@ export function App({ app }: AppProps) {
|
||||
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}
|
||||
<CommandPalette
|
||||
state={commandPalette}
|
||||
document={document}
|
||||
selection={selection}
|
||||
viewport={viewport}
|
||||
tools={tools}
|
||||
generation={generation}
|
||||
layersOpen={layersOpen}
|
||||
activeArtboard={activeArtboard}
|
||||
dispatch={app.store.dispatch}
|
||||
openFilePicker={imageImport.openFilePicker}
|
||||
openGenerate={openGenerate}
|
||||
openLayers={openLayers}
|
||||
closeLayers={closeLayers}
|
||||
/>
|
||||
<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}>
|
||||
@@ -201,6 +240,7 @@ type AppShellState = {
|
||||
viewport: AppState["editor"]["viewport"];
|
||||
tools: AppState["editor"]["tools"];
|
||||
generation: AppState["editor"]["generation"];
|
||||
commandPalette: AppState["editor"]["commandPalette"];
|
||||
transformSession: AppState["editor"]["transformSession"];
|
||||
maskEdit: AppState["editor"]["maskEdit"];
|
||||
};
|
||||
@@ -212,6 +252,7 @@ function selectAppShellState(state: AppState): AppShellState {
|
||||
viewport: state.editor.viewport,
|
||||
tools: state.editor.tools,
|
||||
generation: state.editor.generation,
|
||||
commandPalette: state.editor.commandPalette,
|
||||
transformSession: state.editor.transformSession,
|
||||
maskEdit: state.editor.maskEdit,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user