chore(architecture): enforce layer boundaries

This commit is contained in:
syntaxbullet
2026-07-03 09:34:21 +02:00
parent a8dbd26474
commit d7406e803a
14 changed files with 354 additions and 17 deletions

9
input/AGENTS.md Normal file
View File

@@ -0,0 +1,9 @@
# Input Rules
- `input/` resolves keyboard, pointer, mouse, touch, pen, and wheel input only.
- Resolution order is strict: global consumer first, command fallback second.
- If the global consumer handles input, stop and do not dispatch a command.
- Input handlers must never mutate state directly.
- Input may dispatch commands by id with payloads.
- Keep this layer independent from React, renderer, editor store internals, and persisted core domain models.
- DOM/browser events should be normalized into small input event types before command mapping.

3
input/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export type { CommandKeybind, GlobalKeybindConsumer, Keybind, KeybindEvent, KeybindMap } from "./keyboard";
export { handleKeybind, keybindFromEvent } from "./keyboard";
export type { GlobalPointerConsumer, GlobalWheelConsumer, PointerInputEvent, WheelInputEvent } from "./pointer";

48
input/keyboard.ts Normal file
View File

@@ -0,0 +1,48 @@
import type { Dispatch } from "@commands/dispatcher";
export type Keybind = string;
export type KeybindEvent = {
key: string;
code: string;
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
};
export type GlobalKeybindConsumer = (event: KeybindEvent) => boolean;
export type CommandKeybind = {
commandId: string;
payload?: unknown;
};
export type KeybindMap = ReadonlyMap<Keybind, CommandKeybind>;
export function keybindFromEvent(event: KeybindEvent): Keybind {
const parts = [
event.metaKey ? "Meta" : undefined,
event.ctrlKey ? "Ctrl" : undefined,
event.altKey ? "Alt" : undefined,
event.shiftKey ? "Shift" : undefined,
event.key,
].filter(Boolean);
return parts.join("+");
}
export function handleKeybind(options: {
event: KeybindEvent;
globalConsumer: GlobalKeybindConsumer;
keybindMap: KeybindMap;
dispatch: Dispatch;
}): boolean {
if (options.globalConsumer(options.event)) return true;
const command = options.keybindMap.get(keybindFromEvent(options.event));
if (!command) return false;
options.dispatch(command.commandId, command.payload);
return true;
}

24
input/pointer.ts Normal file
View File

@@ -0,0 +1,24 @@
import type { Vec2D } from "@core/geometry";
export type PointerInputEvent = {
pointerId: number;
pointerType: "mouse" | "pen" | "touch";
position: Vec2D;
buttons: number;
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
};
export type WheelInputEvent = {
position: Vec2D;
delta: Vec2D;
altKey: boolean;
ctrlKey: boolean;
metaKey: boolean;
shiftKey: boolean;
};
export type GlobalPointerConsumer = (event: PointerInputEvent) => boolean;
export type GlobalWheelConsumer = (event: WheelInputEvent) => boolean;