feat: add shortcuts display
This commit is contained in:
@@ -6,6 +6,7 @@ export {
|
|||||||
export type { CommandKeybind, GlobalKeybindConsumer, Keybind, KeybindEvent, KeybindMap } from "./keyboard";
|
export type { CommandKeybind, GlobalKeybindConsumer, Keybind, KeybindEvent, KeybindMap } from "./keyboard";
|
||||||
export { handleKeybind, keybindFromEvent } from "./keyboard";
|
export { handleKeybind, keybindFromEvent } from "./keyboard";
|
||||||
export { handleHistoryKey } from "./history";
|
export { handleHistoryKey } from "./history";
|
||||||
|
export { handleToolKey } from "./tool-keybinds";
|
||||||
export { findGroup, findLayerInfoInDocument, handleDeleteSelectionKey, resolveLayerDrop } from "./layers-panel";
|
export { findGroup, findLayerInfoInDocument, handleDeleteSelectionKey, resolveLayerDrop } from "./layers-panel";
|
||||||
export type { LayerDropTarget, LayerInfo } from "./layers-panel";
|
export type { LayerDropTarget, LayerInfo } from "./layers-panel";
|
||||||
export { handleArtboardSelection } from "./selection";
|
export { handleArtboardSelection } from "./selection";
|
||||||
|
|||||||
20
input/tool-keybinds.ts
Normal file
20
input/tool-keybinds.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { commandIds } from "@commands/ids";
|
||||||
|
import type { Dispatch } from "@commands/dispatcher";
|
||||||
|
import type { KeybindEvent } from "./keyboard";
|
||||||
|
|
||||||
|
const toolKeybinds = {
|
||||||
|
b: "brush",
|
||||||
|
e: "eraser",
|
||||||
|
p: "pan",
|
||||||
|
s: "select",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function handleToolKey(options: { event: KeybindEvent; dispatch: Dispatch }): boolean {
|
||||||
|
if (options.event.altKey || options.event.ctrlKey || options.event.metaKey) return false;
|
||||||
|
|
||||||
|
const tool = toolKeybinds[options.event.key.toLowerCase() as keyof typeof toolKeybinds];
|
||||||
|
if (!tool) return false;
|
||||||
|
|
||||||
|
options.dispatch(commandIds.toolSetActive, { tool });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
19
view/App.tsx
19
view/App.tsx
@@ -6,9 +6,10 @@ import { BottomControlsIsland } from "./BottomControlsIsland";
|
|||||||
import { brushUnavailableHint } from "./canvas/brush";
|
import { brushUnavailableHint } from "./canvas/brush";
|
||||||
import { CanvasViewport } from "./CanvasViewport";
|
import { CanvasViewport } from "./CanvasViewport";
|
||||||
import { LayersSheet } from "./LayersSheet";
|
import { LayersSheet } from "./LayersSheet";
|
||||||
|
import { ShortcutsDisplay } from "./ShortcutsDisplay";
|
||||||
import { ToolOverlay } from "./ToolOverlay";
|
import { ToolOverlay } from "./ToolOverlay";
|
||||||
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
|
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
|
||||||
import { handleDeleteSelectionKey, handleHistoryKey, keybindEventFromKeyboardEvent } from "@input/index";
|
import { handleDeleteSelectionKey, handleHistoryKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index";
|
||||||
import { useAppState } from "./useAppState";
|
import { useAppState } from "./useAppState";
|
||||||
import { downloadArtboardPng } from "./exportArtboardPng";
|
import { downloadArtboardPng } from "./exportArtboardPng";
|
||||||
import { useImageImport } from "./useImageImport";
|
import { useImageImport } from "./useImageImport";
|
||||||
@@ -50,14 +51,23 @@ export function App({ app }: AppProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key.toLowerCase() === "l") {
|
const keybindEvent = keybindEventFromKeyboardEvent(event);
|
||||||
|
const toolConsumed = handleToolKey({ event: keybindEvent, dispatch: app.store.dispatch });
|
||||||
|
if (toolConsumed) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = event.key.toLowerCase();
|
||||||
|
|
||||||
|
if (key === "l") {
|
||||||
setLayersOpen((open) => !open);
|
setLayersOpen((open) => !open);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const consumed = handleDeleteSelectionKey({
|
const consumed = handleDeleteSelectionKey({
|
||||||
event: keybindEventFromKeyboardEvent(event),
|
event: keybindEvent,
|
||||||
selection: app.store.getState().editor.selection,
|
selection: app.store.getState().editor.selection,
|
||||||
dispatch: app.store.dispatch,
|
dispatch: app.store.dispatch,
|
||||||
});
|
});
|
||||||
@@ -115,6 +125,9 @@ export function App({ app }: AppProps) {
|
|||||||
dispatch={app.store.dispatch}
|
dispatch={app.store.dispatch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute bottom-4 left-4 z-10">
|
||||||
|
<ShortcutsDisplay />
|
||||||
|
</div>
|
||||||
<CanvasViewport store={app.store} />
|
<CanvasViewport store={app.store} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
71
view/ShortcutsDisplay.tsx
Normal file
71
view/ShortcutsDisplay.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { ArrowFatLineUp, Backspace, CaretDown, Command } from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
type Shortcut = {
|
||||||
|
keys: string[];
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shortcuts: Shortcut[] = [
|
||||||
|
{ keys: ["S"], label: "Select" },
|
||||||
|
{ keys: ["B"], label: "Brush" },
|
||||||
|
{ keys: ["E"], label: "Eraser" },
|
||||||
|
{ keys: ["P"], label: "Pan" },
|
||||||
|
{ keys: ["Space"], label: "Hold to pan" },
|
||||||
|
{ keys: ["L"], label: "Layers" },
|
||||||
|
{ keys: ["⌘", "O"], label: "Open image" },
|
||||||
|
{ keys: ["⌘", "Z"], label: "Undo" },
|
||||||
|
{ keys: ["⇧", "⌘", "Z"], label: "Redo" },
|
||||||
|
{ keys: ["Del", "⌫"], label: "Delete" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ShortcutsDisplay() {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
aria-label="Keyboard shortcuts"
|
||||||
|
className="pointer-events-auto hidden w-[30rem] rounded-[2rem] p-3 text-white backdrop-blur-xl sm:block"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex h-12 w-full items-center justify-between rounded-full px-5 text-left transition hover:bg-white/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen((current) => !current)}
|
||||||
|
>
|
||||||
|
<span className="text-[0.65rem] font-semibold uppercase tracking-[0.22em] text-white/45">Shortcuts</span>
|
||||||
|
<CaretDown size={14} className={`text-white/40 transition-transform ${open ? "rotate-0" : "-rotate-90"}`} weight="bold" />
|
||||||
|
</button>
|
||||||
|
<dl className={`mt-2 grid grid-cols-2 gap-x-4 gap-y-1 overflow-hidden rounded-[1.5rem] transition-all duration-200 ${open ? "max-h-96 p-1 opacity-100" : "mt-0 max-h-0 p-0 opacity-0"}`}>
|
||||||
|
{shortcuts.map((shortcut) => (
|
||||||
|
<div key={`${shortcut.keys.join("+")}-${shortcut.label}`} className="flex h-8 items-center justify-between gap-3 rounded-full px-2">
|
||||||
|
<dt className="truncate text-xs font-medium text-white/60">{shortcut.label}</dt>
|
||||||
|
<dd className="flex shrink-0 items-center gap-2">
|
||||||
|
{shortcut.keys.map((key, index) => (
|
||||||
|
<span key={key} className="inline-flex items-center gap-2">
|
||||||
|
{shortcut.label === "Delete" && index > 0 ? <span className="h-3 w-px bg-white/20" aria-hidden="true" /> : null}
|
||||||
|
<kbd className="inline-flex h-8 min-w-8 items-center justify-center rounded-full bg-white/5 px-3 text-sm font-semibold leading-none text-white/75">
|
||||||
|
{keyIcon(key) ?? key}
|
||||||
|
</kbd>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyIcon(key: string) {
|
||||||
|
switch (key) {
|
||||||
|
case "⌘":
|
||||||
|
return <Command size={18} weight="bold" />;
|
||||||
|
case "⇧":
|
||||||
|
return <ArrowFatLineUp size={18} weight="bold" />;
|
||||||
|
case "⌫":
|
||||||
|
return <Backspace size={18} weight="bold" />;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user