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:
@@ -121,6 +121,11 @@ function snapshot(state: AppState): HistorySnapshot {
|
||||
...state.editor,
|
||||
brushPreview: undefined,
|
||||
brushStrokePreview: undefined,
|
||||
commandPalette: {
|
||||
open: false,
|
||||
query: "",
|
||||
selectedIndex: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,4 +55,8 @@ export const commandIds = {
|
||||
viewportFitArtboard: "viewport.fitArtboard",
|
||||
historyUndo: "history.undo",
|
||||
historyRedo: "history.redo",
|
||||
commandPaletteOpen: "commandPalette.open",
|
||||
commandPaletteClose: "commandPalette.close",
|
||||
commandPaletteSetQuery: "commandPalette.setQuery",
|
||||
commandPaletteSetSelectedIndex: "commandPalette.setSelectedIndex",
|
||||
} as const;
|
||||
|
||||
@@ -71,6 +71,18 @@ export type {
|
||||
export type { CommandDispatcher, Dispatch } from "./dispatcher";
|
||||
export type { CommandId, CommandPayloads } from "./payloads";
|
||||
export { createCommandDispatcher } from "./dispatcher";
|
||||
export {
|
||||
commandPaletteCloseCommand,
|
||||
commandPaletteCommands,
|
||||
commandPaletteOpenCommand,
|
||||
commandPaletteSetQueryCommand,
|
||||
commandPaletteSetSelectedIndexCommand,
|
||||
} from "./palette";
|
||||
export type {
|
||||
CommandPaletteOpenPayload,
|
||||
CommandPaletteSetQueryPayload,
|
||||
CommandPaletteSetSelectedIndexPayload,
|
||||
} from "./palette";
|
||||
export type { CommandRegistry } from "./registry";
|
||||
export { createCommandRegistry } from "./registry";
|
||||
export { selectionAddLayerCommand, selectionClearCommand, selectionCommands, selectionSetCommand } from "./selection";
|
||||
|
||||
40
commands/palette.test.ts
Normal file
40
commands/palette.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { createInitialAppState } from "@editor/initial-state";
|
||||
import {
|
||||
commandPaletteCloseCommand,
|
||||
commandPaletteOpenCommand,
|
||||
commandPaletteSetQueryCommand,
|
||||
commandPaletteSetSelectedIndexCommand,
|
||||
} from "./palette";
|
||||
|
||||
describe("command palette commands", () => {
|
||||
test("opens with a reset query and selection", () => {
|
||||
const initial = commandPaletteSetQueryCommand.execute({ state: createInitialAppState("Test") }, { query: "brush" });
|
||||
const next = commandPaletteOpenCommand.execute({ state: initial }, undefined);
|
||||
|
||||
expect(next.editor.commandPalette).toEqual({ open: true, query: "", selectedIndex: 0 });
|
||||
});
|
||||
|
||||
test("sets query and returns selected item to the first result", () => {
|
||||
const opened = commandPaletteOpenCommand.execute({ state: createInitialAppState("Test") }, { query: "layer", selectedIndex: 4 });
|
||||
const next = commandPaletteSetQueryCommand.execute({ state: opened }, { query: "zoom" });
|
||||
|
||||
expect(next.editor.commandPalette).toEqual({ open: true, query: "zoom", selectedIndex: 0 });
|
||||
});
|
||||
|
||||
test("clamps selected index to a non-negative integer", () => {
|
||||
const opened = commandPaletteOpenCommand.execute({ state: createInitialAppState("Test") }, undefined);
|
||||
const fractional = commandPaletteSetSelectedIndexCommand.execute({ state: opened }, { selectedIndex: 3.8 });
|
||||
const negative = commandPaletteSetSelectedIndexCommand.execute({ state: opened }, { selectedIndex: -1 });
|
||||
|
||||
expect(fractional.editor.commandPalette.selectedIndex).toBe(3);
|
||||
expect(negative.editor.commandPalette.selectedIndex).toBe(0);
|
||||
});
|
||||
|
||||
test("closes and clears transient palette text", () => {
|
||||
const opened = commandPaletteOpenCommand.execute({ state: createInitialAppState("Test") }, { query: "debug", selectedIndex: 2 });
|
||||
const next = commandPaletteCloseCommand.execute({ state: opened }, undefined);
|
||||
|
||||
expect(next.editor.commandPalette).toEqual({ open: false, query: "", selectedIndex: 0 });
|
||||
});
|
||||
});
|
||||
121
commands/palette.ts
Normal file
121
commands/palette.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { Command } from "./command";
|
||||
import { commandIds } from "./ids";
|
||||
|
||||
export type CommandPaletteOpenPayload = {
|
||||
query?: string;
|
||||
selectedIndex?: number;
|
||||
} | undefined;
|
||||
|
||||
export type CommandPaletteSetQueryPayload = {
|
||||
query: string;
|
||||
};
|
||||
|
||||
export type CommandPaletteSetSelectedIndexPayload = {
|
||||
selectedIndex: number;
|
||||
};
|
||||
|
||||
export const commandPaletteOpenCommand: Command<CommandPaletteOpenPayload> = {
|
||||
id: commandIds.commandPaletteOpen,
|
||||
name: "Open command palette",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
const commandPalette = {
|
||||
open: true,
|
||||
query: payload?.query ?? "",
|
||||
selectedIndex: normalizeSelectedIndex(payload?.selectedIndex ?? 0),
|
||||
};
|
||||
if (
|
||||
state.editor.commandPalette.open === commandPalette.open &&
|
||||
state.editor.commandPalette.query === commandPalette.query &&
|
||||
state.editor.commandPalette.selectedIndex === commandPalette.selectedIndex
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
commandPalette,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const commandPaletteCloseCommand: Command = {
|
||||
id: commandIds.commandPaletteClose,
|
||||
name: "Close command palette",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }) {
|
||||
const commandPalette = { open: false, query: "", selectedIndex: 0 };
|
||||
if (
|
||||
state.editor.commandPalette.open === commandPalette.open &&
|
||||
state.editor.commandPalette.query === commandPalette.query &&
|
||||
state.editor.commandPalette.selectedIndex === commandPalette.selectedIndex
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
commandPalette,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const commandPaletteSetQueryCommand: Command<CommandPaletteSetQueryPayload> = {
|
||||
id: commandIds.commandPaletteSetQuery,
|
||||
name: "Set command palette query",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
if (state.editor.commandPalette.query === payload.query && state.editor.commandPalette.selectedIndex === 0) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
commandPalette: {
|
||||
...state.editor.commandPalette,
|
||||
query: payload.query,
|
||||
selectedIndex: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const commandPaletteSetSelectedIndexCommand: Command<CommandPaletteSetSelectedIndexPayload> = {
|
||||
id: commandIds.commandPaletteSetSelectedIndex,
|
||||
name: "Set command palette selected index",
|
||||
history: { mode: "ignore" },
|
||||
execute({ state }, payload) {
|
||||
const selectedIndex = normalizeSelectedIndex(payload.selectedIndex);
|
||||
if (state.editor.commandPalette.selectedIndex === selectedIndex) return state;
|
||||
|
||||
return {
|
||||
...state,
|
||||
editor: {
|
||||
...state.editor,
|
||||
commandPalette: {
|
||||
...state.editor.commandPalette,
|
||||
selectedIndex,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const commandPaletteCommands = [
|
||||
commandPaletteOpenCommand,
|
||||
commandPaletteCloseCommand,
|
||||
commandPaletteSetQueryCommand,
|
||||
commandPaletteSetSelectedIndexCommand,
|
||||
] satisfies Command<unknown>[];
|
||||
|
||||
function normalizeSelectedIndex(value: number) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.floor(value));
|
||||
}
|
||||
@@ -31,6 +31,11 @@ import type {
|
||||
GenerationSelectCandidatePayload,
|
||||
GenerationSetCompareModePayload,
|
||||
} from "./generation";
|
||||
import type {
|
||||
CommandPaletteOpenPayload,
|
||||
CommandPaletteSetQueryPayload,
|
||||
CommandPaletteSetSelectedIndexPayload,
|
||||
} from "./palette";
|
||||
import type { SelectionAddLayerPayload, SelectionSetPayload } from "./selection";
|
||||
import type { ToolEnterMaskEditPayload, ToolSetActivePayload, ToolSetBrushPreviewPayload, ToolSetBrushSettingsPayload, ToolSetBrushStrokePreviewPayload, ToolSetChromaKeySettingsPayload, ToolSetGenerateSettingsPayload, ToolSetMagicWandSettingsPayload, ToolSetMaskViewModePayload } from "./tool";
|
||||
import type { TransformBeginPayload, TransformSetBoundsPayload, TransformUpdatePayload } from "./transform";
|
||||
@@ -99,6 +104,10 @@ export type CommandPayloads = {
|
||||
[commandIds.viewportFitArtboard]: ViewportFitArtboardPayload | undefined;
|
||||
[commandIds.historyUndo]: void;
|
||||
[commandIds.historyRedo]: void;
|
||||
[commandIds.commandPaletteOpen]: CommandPaletteOpenPayload;
|
||||
[commandIds.commandPaletteClose]: void;
|
||||
[commandIds.commandPaletteSetQuery]: CommandPaletteSetQueryPayload;
|
||||
[commandIds.commandPaletteSetSelectedIndex]: CommandPaletteSetSelectedIndexPayload;
|
||||
};
|
||||
|
||||
export type CommandId = keyof CommandPayloads;
|
||||
|
||||
Reference in New Issue
Block a user