feat: implement document actions for managing artboards, zoom, and history; update UI components to utilize new actions
This commit is contained in:
@@ -14,6 +14,7 @@ import { projectCommands } from "@commands/project";
|
||||
import { createInitialAppState } from "@editor/initial-state";
|
||||
import { createAppStore } from "@editor/store";
|
||||
import { createGenerationWorkflow } from "@operations/generation/workflow";
|
||||
import { createDocumentActions } from "./document-actions";
|
||||
|
||||
export type ImageStudioApp = ReturnType<typeof createImageStudioApp>;
|
||||
|
||||
@@ -21,6 +22,7 @@ export function createImageStudioApp(options?: { documentName?: string; createDe
|
||||
const registry = createCommandRegistry([...projectCommands, ...viewportCommands, ...selectionCommands, ...documentCommands, ...toolCommands, ...generationCommands, ...transformCommands, ...historyCommands, ...commandPaletteCommands, ...workspaceCommands, ...editorCommands]);
|
||||
const store = createAppStore(createInitialAppState(options?.documentName), registry);
|
||||
const generation = createGenerationWorkflow(store);
|
||||
const documentActions = createDocumentActions(store, generation);
|
||||
|
||||
if (options?.createDefaultArtboard !== false) {
|
||||
const artboardId = crypto.randomUUID();
|
||||
@@ -29,12 +31,13 @@ export function createImageStudioApp(options?: { documentName?: string; createDe
|
||||
name: "Artboard 1",
|
||||
bounds: { x: -400, y: -300, w: 800, h: 600 },
|
||||
});
|
||||
store.dispatch(commandIds.viewportFitArtboard, { artboardId });
|
||||
documentActions.fitArtboard(artboardId);
|
||||
}
|
||||
|
||||
return {
|
||||
registry,
|
||||
store,
|
||||
actions: { document: documentActions },
|
||||
workflows: { generation },
|
||||
};
|
||||
}
|
||||
|
||||
34
app/document-actions.test.ts
Normal file
34
app/document-actions.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import { createImageStudioApp } from "./app";
|
||||
|
||||
describe("document actions", () => {
|
||||
test("share zoom, fit, and history behavior across UI entry points", () => {
|
||||
const app = createImageStudioApp();
|
||||
const artboard = app.store.getState().document.artboards[0];
|
||||
if (!artboard) throw new Error("Expected the default artboard.");
|
||||
|
||||
app.actions.document.zoomTo(2);
|
||||
app.actions.document.zoomBy(0.5);
|
||||
expect(app.store.getState().editor.viewport.zoom).toBe(1);
|
||||
|
||||
app.actions.document.fitArtboard(artboard.id);
|
||||
expect(app.store.getState().editor.viewport.center).toEqual({ x: 0, y: 0 });
|
||||
|
||||
expect(app.actions.document.canUndo()).toBe(true);
|
||||
app.actions.document.undo();
|
||||
expect(app.store.getState().document.artboards).toHaveLength(0);
|
||||
expect(app.actions.document.canRedo()).toBe(true);
|
||||
app.actions.document.redo();
|
||||
expect(app.store.getState().document.artboards).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("opens Generate through the canonical workspace command", () => {
|
||||
const app = createImageStudioApp({ createDefaultArtboard: false });
|
||||
app.actions.document.openGenerate();
|
||||
expect(app.store.getState().editor.workspace.panel).toBe("generate");
|
||||
|
||||
app.store.dispatch(commandIds.workspaceSetPanel, { panel: "none" });
|
||||
expect(app.store.getState().editor.workspace.panel).toBe("none");
|
||||
});
|
||||
});
|
||||
58
app/document-actions.ts
Normal file
58
app/document-actions.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { commandIds } from "@commands/ids";
|
||||
import type { ArtboardId } from "@core/id";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import { downloadArtboardPng } from "@operations/export/downloadArtboard";
|
||||
import type { GenerationWorkflow } from "@operations/generation/workflow";
|
||||
|
||||
export type DocumentActions = ReturnType<typeof createDocumentActions>;
|
||||
|
||||
export function createDocumentActions(store: AppStore, generation: GenerationWorkflow) {
|
||||
return {
|
||||
openGenerate() {
|
||||
store.dispatch(commandIds.workspaceSetPanel, { panel: "generate" });
|
||||
},
|
||||
|
||||
generate() {
|
||||
return generation.generate();
|
||||
},
|
||||
|
||||
exportArtboard(artboardId?: ArtboardId) {
|
||||
const state = store.getState();
|
||||
const resolvedId = artboardId ?? state.editor.selection.artboardId;
|
||||
const artboard = resolvedId
|
||||
? state.document.artboards.find((candidate) => candidate.id === resolvedId)
|
||||
: state.document.artboards[0];
|
||||
if (!artboard) return Promise.resolve();
|
||||
return downloadArtboardPng(artboard, state.document.assets);
|
||||
},
|
||||
|
||||
fitArtboard(artboardId?: ArtboardId) {
|
||||
store.dispatch(commandIds.viewportFitArtboard, artboardId ? { artboardId } : undefined);
|
||||
},
|
||||
|
||||
zoomBy(factor: number) {
|
||||
const zoom = store.getState().editor.viewport.zoom;
|
||||
store.dispatch(commandIds.viewportSetZoom, { zoom: zoom * factor });
|
||||
},
|
||||
|
||||
zoomTo(zoom: number) {
|
||||
store.dispatch(commandIds.viewportSetZoom, { zoom });
|
||||
},
|
||||
|
||||
undo() {
|
||||
store.dispatch(commandIds.historyUndo, undefined);
|
||||
},
|
||||
|
||||
canUndo() {
|
||||
return store.getState().history.past.length > 0;
|
||||
},
|
||||
|
||||
redo() {
|
||||
store.dispatch(commandIds.historyRedo, undefined);
|
||||
},
|
||||
|
||||
canRedo() {
|
||||
return store.getState().history.future.length > 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export type { ImageStudioApp } from "./app";
|
||||
export { createImageStudioApp } from "./app";
|
||||
export type { DocumentActions } from "./document-actions";
|
||||
|
||||
@@ -2,11 +2,20 @@ import { commandIds } from "@commands/ids";
|
||||
import type { Dispatch } from "@commands/dispatcher";
|
||||
import type { KeybindEvent } from "./keyboard";
|
||||
|
||||
export function handleHistoryKey(options: { event: KeybindEvent; dispatch: Dispatch }): boolean {
|
||||
export type HistoryActions = { undo(): void; redo(): void };
|
||||
|
||||
export function handleHistoryKey(options: { event: KeybindEvent; dispatch?: Dispatch; actions?: HistoryActions }): boolean {
|
||||
if (options.event.altKey) return false;
|
||||
const modifier = options.event.metaKey || options.event.ctrlKey;
|
||||
if (!modifier || options.event.key.toLowerCase() !== "z") return false;
|
||||
|
||||
if (options.actions) {
|
||||
if (options.event.shiftKey) options.actions.redo();
|
||||
else options.actions.undo();
|
||||
} else if (options.dispatch) {
|
||||
options.dispatch(options.event.shiftKey ? commandIds.historyRedo : commandIds.historyUndo, undefined);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
14
view/App.tsx
14
view/App.tsx
@@ -15,7 +15,6 @@ import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/t
|
||||
import type { AppState } from "@editor/state";
|
||||
import { handleCommandPaletteKey, handleDeleteSelectionKey, handleHistoryKey, handleOperationKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index";
|
||||
import { shallowEqual, useAppState } from "./useAppState";
|
||||
import { downloadArtboardPng } from "@operations/export/downloadArtboard";
|
||||
import { useImageImport } from "./useImageImport";
|
||||
import { useViewportActivityIsland } from "./useViewportActivityIsland";
|
||||
import { useProjectLifecycle } from "./useProjectLifecycle";
|
||||
@@ -42,8 +41,8 @@ export function App({ app }: AppProps) {
|
||||
}, [app.workflows.generation, generateOpen]);
|
||||
|
||||
const openGenerate = useCallback(() => {
|
||||
app.store.dispatch(commandIds.workspaceSetPanel, { panel: "generate" });
|
||||
}, [app.store]);
|
||||
app.actions.document.openGenerate();
|
||||
}, [app.actions.document]);
|
||||
|
||||
const closeGenerate = useCallback(() => {
|
||||
app.store.dispatch(commandIds.workspaceSetPanel, { panel: "none" });
|
||||
@@ -90,7 +89,7 @@ export function App({ app }: AppProps) {
|
||||
|
||||
if (editableTarget) return;
|
||||
|
||||
const historyConsumed = handleHistoryKey({ event: keybindEvent, dispatch: app.store.dispatch });
|
||||
const historyConsumed = handleHistoryKey({ event: keybindEvent, actions: app.actions.document });
|
||||
if (historyConsumed) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
@@ -134,7 +133,7 @@ export function App({ app }: AppProps) {
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [app.store, toggleLayers]);
|
||||
}, [app.actions.document, app.store, toggleLayers]);
|
||||
const transformBounds = transformTarget ? resolveTransformTargetBounds(document, transformTarget) : undefined;
|
||||
const brushHint = brushUnavailableHint(document, { selection, tools, maskEdit });
|
||||
|
||||
@@ -156,6 +155,7 @@ export function App({ app }: AppProps) {
|
||||
openGenerate={openGenerate}
|
||||
openLayers={openLayers}
|
||||
closeLayers={closeLayers}
|
||||
documentActions={app.actions.document}
|
||||
/>
|
||||
<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">
|
||||
@@ -174,7 +174,7 @@ export function App({ app }: AppProps) {
|
||||
<button type="button" className={topBarButtonClass(generateOpen)} aria-pressed={generateOpen} onClick={toggleGenerate}>
|
||||
<Sparkle size={24} weight={generateOpen ? "fill" : "regular"} />
|
||||
</button>
|
||||
<button type="button" className={topBarButtonClass()} disabled={!activeArtboard} onClick={() => activeArtboard && void downloadArtboardPng(activeArtboard, document.assets)}>
|
||||
<button type="button" className={topBarButtonClass()} disabled={!activeArtboard} onClick={() => void app.actions.document.exportArtboard(activeArtboard?.id)}>
|
||||
<DownloadSimple size={24} />
|
||||
</button>
|
||||
<button type="button" className={topBarButtonClass(layersOpen)} aria-pressed={layersOpen} onClick={toggleLayers}>
|
||||
@@ -202,6 +202,7 @@ export function App({ app }: AppProps) {
|
||||
maskEdit={maskEdit}
|
||||
open={layersOpen}
|
||||
dispatch={app.store.dispatch}
|
||||
documentActions={app.actions.document}
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-4 z-10 flex justify-center">
|
||||
<BottomControlsIsland
|
||||
@@ -224,6 +225,7 @@ export function App({ app }: AppProps) {
|
||||
transformTarget={viewportActivityIsland.visible ? undefined : transformTarget}
|
||||
dispatch={app.store.dispatch}
|
||||
generationWorkflow={app.workflows.generation}
|
||||
documentActions={app.actions.document}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-4 left-4 z-10">
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ZoomControls } from "./bottom-controls/ZoomControls";
|
||||
import type { Rect } from "@core/geometry";
|
||||
import type { TransformTarget } from "@editor/transform";
|
||||
import type { GenerationWorkflow } from "@operations/generation/workflow";
|
||||
import type { DocumentActions } from "@app/document-actions";
|
||||
export type BottomControlsAction = "pan" | "zoom";
|
||||
|
||||
export type BottomControlsIslandProps = {
|
||||
@@ -34,9 +35,10 @@ export type BottomControlsIslandProps = {
|
||||
brushHint?: string;
|
||||
dispatch: AppStore["dispatch"];
|
||||
generationWorkflow: GenerationWorkflow;
|
||||
documentActions: DocumentActions;
|
||||
};
|
||||
|
||||
export function BottomControlsIsland({ document, selection, viewport, visible, action, activeTool, operation, brushSettings, generateSettings, generation, chromaKeySettings, magicWandSettings, editingMask = false, maskViewMode = "composite", transformBounds, transformTarget, brushHint, dispatch, generationWorkflow }: BottomControlsIslandProps) {
|
||||
export function BottomControlsIsland({ document, selection, viewport, visible, action, activeTool, operation, brushSettings, generateSettings, generation, chromaKeySettings, magicWandSettings, editingMask = false, maskViewMode = "composite", transformBounds, transformTarget, brushHint, dispatch, generationWorkflow, documentActions }: BottomControlsIslandProps) {
|
||||
const zoomPercent = Math.round(viewport.zoom * 100);
|
||||
const x = Math.round(viewport.center.x);
|
||||
const y = Math.round(viewport.center.y);
|
||||
@@ -49,7 +51,7 @@ export function BottomControlsIsland({ document, selection, viewport, visible, a
|
||||
}`}
|
||||
>
|
||||
{operation === "generate" ? (
|
||||
<GenerateActionControls settings={generateSettings} generation={generation} dispatch={dispatch} workflow={generationWorkflow} />
|
||||
<GenerateActionControls settings={generateSettings} generation={generation} dispatch={dispatch} workflow={generationWorkflow} generate={documentActions.generate} />
|
||||
) : (activeTool === "brush" || activeTool === "eraser") && brushHint ? (
|
||||
<BrushHint tool={activeTool} hint={brushHint} />
|
||||
) : activeTool === "brush" || activeTool === "eraser" ? (
|
||||
@@ -63,7 +65,7 @@ export function BottomControlsIsland({ document, selection, viewport, visible, a
|
||||
) : action === "pan" ? (
|
||||
<PanControls x={x} y={y} />
|
||||
) : (
|
||||
<ZoomControls zoom={viewport.zoom} zoomPercent={zoomPercent} dispatch={dispatch} />
|
||||
<ZoomControls zoomPercent={zoomPercent} actions={documentActions} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { AppStore } from "@editor/store";
|
||||
import type { ToolState } from "@editor/tools";
|
||||
import { createDocumentReadIndex } from "@editor/document-indexes";
|
||||
import { createPaletteItems, type PaletteItem } from "./paletteItems";
|
||||
import type { DocumentActions } from "@app/document-actions";
|
||||
|
||||
export type CommandPaletteProps = {
|
||||
state: CommandPaletteState;
|
||||
@@ -23,6 +24,7 @@ export type CommandPaletteProps = {
|
||||
openGenerate: () => void;
|
||||
openLayers: () => void;
|
||||
closeLayers: () => void;
|
||||
documentActions: DocumentActions;
|
||||
};
|
||||
|
||||
export function CommandPalette({
|
||||
@@ -39,6 +41,7 @@ export function CommandPalette({
|
||||
openGenerate,
|
||||
openLayers,
|
||||
closeLayers,
|
||||
documentActions,
|
||||
}: CommandPaletteProps) {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const documentIndex = useMemo(() => createDocumentReadIndex(document), [document]);
|
||||
@@ -67,6 +70,7 @@ export function CommandPalette({
|
||||
openGenerate,
|
||||
openLayers,
|
||||
closeLayers,
|
||||
documentActions,
|
||||
}),
|
||||
[
|
||||
activeArtboard,
|
||||
@@ -77,6 +81,7 @@ export function CommandPalette({
|
||||
dispatch,
|
||||
document,
|
||||
documentIndex,
|
||||
documentActions,
|
||||
generation,
|
||||
layersOpen,
|
||||
openFilePicker,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createDocumentReadIndex, type DocumentReadIndex } from "@editor/documen
|
||||
import type { MaskEditState, SelectionState } from "@editor/state";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import { resolveLayerDrop } from "@input/index";
|
||||
import { downloadArtboardPng } from "@operations/export/downloadArtboard";
|
||||
import type { DocumentActions } from "@app/document-actions";
|
||||
import { addArtboard, addEmptyLayer, addGroupLayer, addLayerMask, deleteSelection, groupLayers, moveLayer } from "@operations/document/layerActions";
|
||||
import { MaskOperationButtons, MaskStatus } from "./layers/MaskControls";
|
||||
|
||||
@@ -19,9 +19,10 @@ export type LayersSheetProps = {
|
||||
maskEdit?: MaskEditState;
|
||||
open: boolean;
|
||||
dispatch: AppStore["dispatch"];
|
||||
documentActions: DocumentActions;
|
||||
};
|
||||
|
||||
export function LayersSheet({ document, selection, maskEdit, open, dispatch }: LayersSheetProps) {
|
||||
export function LayersSheet({ document, selection, maskEdit, open, dispatch, documentActions }: LayersSheetProps) {
|
||||
const draggedLayerId = useRef<string | undefined>(undefined);
|
||||
const [editingTitle, setEditingTitle] = useState<EditingTitle>();
|
||||
|
||||
@@ -41,6 +42,7 @@ export function LayersSheet({ document, selection, maskEdit, open, dispatch }: L
|
||||
editingTitle={editingTitle}
|
||||
setEditingTitle={setEditingTitle}
|
||||
dispatch={dispatch}
|
||||
documentActions={documentActions}
|
||||
/>
|
||||
) : null}
|
||||
</aside>
|
||||
@@ -55,6 +57,7 @@ function LayersSheetBody({
|
||||
editingTitle,
|
||||
setEditingTitle,
|
||||
dispatch,
|
||||
documentActions,
|
||||
}: Omit<LayersSheetProps, "open"> & {
|
||||
draggedLayerId: MutableRefObject<string | undefined>;
|
||||
editingTitle: EditingTitle | undefined;
|
||||
@@ -152,7 +155,7 @@ function LayersSheetBody({
|
||||
title="Export PNG"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
void downloadArtboardPng(artboard, document.assets);
|
||||
void documentActions.exportArtboard(artboard.id);
|
||||
}}
|
||||
>
|
||||
<DownloadSimple size={24} weight="regular" />
|
||||
|
||||
@@ -10,9 +10,10 @@ export type GenerateActionControlsProps = {
|
||||
generation: GenerationState;
|
||||
dispatch: AppStore["dispatch"];
|
||||
workflow: GenerationWorkflow;
|
||||
generate: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function GenerateActionControls({ settings, generation, dispatch, workflow }: GenerateActionControlsProps) {
|
||||
export function GenerateActionControls({ settings, generation, dispatch, workflow, generate }: GenerateActionControlsProps) {
|
||||
const job = currentGenerationJob(generation);
|
||||
const busy = job?.status === "running";
|
||||
const candidate = selectedCandidate(generation);
|
||||
@@ -28,7 +29,7 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
||||
className="h-12 rounded-full bg-white px-7 text-base font-semibold !text-black transition hover:bg-white/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/40 disabled:pointer-events-none disabled:opacity-35"
|
||||
title={job?.status === "failed" ? job.error : preconditionMessage ?? "Generate with ComfyUI"}
|
||||
onClick={() => {
|
||||
void workflow.generate();
|
||||
void generate();
|
||||
}}
|
||||
>
|
||||
{busy && job?.kind === "generate" ? "Generating..." : "Generate"}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import { Minus, CornersOut, Plus } from "@phosphor-icons/react";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import type { AppStore } from "@editor/store";
|
||||
import type { DocumentActions } from "@app/document-actions";
|
||||
import { BottomControlDivider } from "./Divider";
|
||||
import { bottomControlButtonClass, bottomControlMenuClass, bottomControlValueClass } from "./styles";
|
||||
|
||||
export type ZoomControlsProps = {
|
||||
zoom: number;
|
||||
zoomPercent: number;
|
||||
dispatch: AppStore["dispatch"];
|
||||
actions: DocumentActions;
|
||||
};
|
||||
|
||||
export function ZoomControls({ zoom, zoomPercent, dispatch }: ZoomControlsProps) {
|
||||
export function ZoomControls({ zoomPercent, actions }: ZoomControlsProps) {
|
||||
return (
|
||||
<div className={bottomControlMenuClass()}>
|
||||
<button type="button" className={bottomControlButtonClass()} aria-label="Zoom out" onClick={() => dispatch(commandIds.viewportSetZoom, { zoom: zoom / 1.2 })}>
|
||||
<button type="button" className={bottomControlButtonClass()} aria-label="Zoom out" onClick={() => actions.zoomBy(1 / 1.2)}>
|
||||
<Minus size={24} weight="regular" />
|
||||
</button>
|
||||
<span className={bottomControlValueClass()}>{zoomPercent}%</span>
|
||||
<button type="button" className={bottomControlButtonClass()} aria-label="Zoom in" onClick={() => dispatch(commandIds.viewportSetZoom, { zoom: zoom * 1.2 })}>
|
||||
<button type="button" className={bottomControlButtonClass()} aria-label="Zoom in" onClick={() => actions.zoomBy(1.2)}>
|
||||
<Plus size={24} weight="regular" />
|
||||
</button>
|
||||
<BottomControlDivider />
|
||||
<button type="button" className={bottomControlButtonClass()} aria-label="Fit artboard" title="Fit artboard" onClick={() => dispatch(commandIds.viewportFitArtboard, undefined)}>
|
||||
<button type="button" className={bottomControlButtonClass()} aria-label="Fit artboard" title="Fit artboard" onClick={() => actions.fitArtboard()}>
|
||||
<CornersOut size={24} weight="regular" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowDown, ArrowUp, CornersOut, Cursor, DownloadSimple, DropHalf, Eraser, Eye, EyeSlash, FolderOpen, FolderPlus, Hand, Lock, LockOpen, MagicWand, Minus, PaintBrush, Plus, Sparkle, Stack, Trash } from "@phosphor-icons/react";
|
||||
import { ArrowCounterClockwise, ArrowDown, ArrowUp, CornersOut, Cursor, DownloadSimple, DropHalf, Eraser, Eye, EyeSlash, FolderOpen, FolderPlus, Hand, Lock, LockOpen, MagicWand, Minus, PaintBrush, Plus, Sparkle, Stack, Trash } from "@phosphor-icons/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { commandIds } from "@commands/ids";
|
||||
import type { Artboard } from "@core/artboard";
|
||||
@@ -7,7 +7,7 @@ import type { GenerationCompareMode, GenerationState, SelectionState, ViewportSt
|
||||
import type { AppStore } from "@editor/store";
|
||||
import { availableToolIds, type GenerateMode, type ToolId, type ToolState } from "@editor/tools";
|
||||
import type { DocumentReadIndex, IndexedLayerInfo } from "@editor/document-indexes";
|
||||
import { downloadArtboardPng } from "@operations/export/downloadArtboard";
|
||||
import type { DocumentActions } from "@app/document-actions";
|
||||
import { addArtboard, addEmptyLayer, addGroupLayer, deleteSelection, groupLayers, moveLayer } from "@operations/document/layerActions";
|
||||
import { labelForTool } from "./toolLabels";
|
||||
|
||||
@@ -31,6 +31,7 @@ export function createPaletteItems(options: {
|
||||
openGenerate: () => void;
|
||||
openLayers: () => void;
|
||||
closeLayers: () => void;
|
||||
documentActions: DocumentActions;
|
||||
}): PaletteItem[] {
|
||||
const {
|
||||
document,
|
||||
@@ -50,6 +51,7 @@ export function createPaletteItems(options: {
|
||||
openGenerate,
|
||||
openLayers,
|
||||
closeLayers,
|
||||
documentActions,
|
||||
} = options;
|
||||
const hasCandidates = generation.candidates.length > 0;
|
||||
const items: PaletteItem[] = [];
|
||||
@@ -85,12 +87,30 @@ export function createPaletteItems(options: {
|
||||
disabled: !activeArtboard,
|
||||
icon: <DownloadSimple size={20} />,
|
||||
run: () => {
|
||||
if (activeArtboard) void downloadArtboardPng(activeArtboard, document.assets);
|
||||
if (activeArtboard) void documentActions.exportArtboard(activeArtboard.id);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: "undo",
|
||||
section: "Edit",
|
||||
title: "Undo",
|
||||
subtitle: "Cmd/Ctrl+Z",
|
||||
disabled: !documentActions.canUndo(),
|
||||
icon: <ArrowCounterClockwise size={20} />,
|
||||
run: documentActions.undo,
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
section: "Edit",
|
||||
title: "Redo",
|
||||
subtitle: "Cmd/Ctrl+Shift+Z",
|
||||
disabled: !documentActions.canRedo(),
|
||||
icon: <ArrowCounterClockwise size={20} className="scale-x-[-1]" />,
|
||||
run: documentActions.redo,
|
||||
},
|
||||
{
|
||||
id: layersOpen ? "close-layers" : "open-layers",
|
||||
section: "Layers",
|
||||
@@ -271,7 +291,7 @@ export function createPaletteItems(options: {
|
||||
title: "Zoom in",
|
||||
subtitle: `${Math.round(viewport.zoom * 100)}%`,
|
||||
icon: <Plus size={20} />,
|
||||
run: () => dispatch(commandIds.viewportSetZoom, { zoom: viewport.zoom * 1.2 }),
|
||||
run: () => documentActions.zoomBy(1.2),
|
||||
},
|
||||
{
|
||||
id: "zoom-out",
|
||||
@@ -279,14 +299,14 @@ export function createPaletteItems(options: {
|
||||
title: "Zoom out",
|
||||
subtitle: `${Math.round(viewport.zoom * 100)}%`,
|
||||
icon: <Minus size={20} />,
|
||||
run: () => dispatch(commandIds.viewportSetZoom, { zoom: viewport.zoom / 1.2 }),
|
||||
run: () => documentActions.zoomBy(1 / 1.2),
|
||||
},
|
||||
{
|
||||
id: "zoom-100",
|
||||
section: "Zoom",
|
||||
title: "Zoom to 100%",
|
||||
icon: <CornersOut size={20} />,
|
||||
run: () => dispatch(commandIds.viewportSetZoom, { zoom: 1 }),
|
||||
run: () => documentActions.zoomTo(1),
|
||||
},
|
||||
{
|
||||
id: "fit-artboard",
|
||||
@@ -295,34 +315,7 @@ export function createPaletteItems(options: {
|
||||
subtitle: activeArtboard?.name,
|
||||
disabled: !activeArtboard,
|
||||
icon: <CornersOut size={20} />,
|
||||
run: () => dispatch(commandIds.viewportFitArtboard, undefined),
|
||||
},
|
||||
);
|
||||
|
||||
items.push(
|
||||
{
|
||||
id: "debug-reset-viewport",
|
||||
section: "Debug",
|
||||
title: "Reset viewport",
|
||||
icon: <CornersOut size={20} />,
|
||||
run: () => dispatch(commandIds.viewportReset, undefined),
|
||||
},
|
||||
{
|
||||
id: "debug-clear-selection",
|
||||
section: "Debug",
|
||||
title: "Clear selection",
|
||||
subtitle: selection.layerIds.length > 0 || selection.artboardId ? undefined : "Nothing selected",
|
||||
disabled: selection.layerIds.length === 0 && !selection.artboardId,
|
||||
icon: <Cursor size={20} />,
|
||||
run: () => dispatch(commandIds.selectionClear, undefined),
|
||||
},
|
||||
{
|
||||
id: "debug-clear-candidates",
|
||||
section: "Debug",
|
||||
title: "Clear generation state",
|
||||
disabled: !hasCandidates,
|
||||
icon: <Trash size={20} />,
|
||||
run: () => dispatch(commandIds.generationClearCandidates, undefined),
|
||||
run: () => documentActions.fitArtboard(activeArtboard?.id),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user