@pixerate/editor 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -2
- package/THIRD_PARTY_NOTICES.md +88 -0
- package/dist/agent/index.cjs +83 -108
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +69 -211
- package/dist/agent/index.d.ts +69 -211
- package/dist/agent/index.js +76 -101
- package/dist/agent/index.js.map +1 -1
- package/dist/arrangeShortcuts-B8O3f8-P.d.cts +184 -0
- package/dist/arrangeShortcuts-sLlD2rSX.d.ts +184 -0
- package/dist/canvas/index.cjs +879 -83
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +291 -186
- package/dist/canvas/index.d.ts +291 -186
- package/dist/canvas/index.js +865 -69
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-26BVFKHU.cjs +453 -0
- package/dist/chunk-26BVFKHU.cjs.map +1 -0
- package/dist/chunk-2H7ZHPEJ.js +762 -0
- package/dist/chunk-2H7ZHPEJ.js.map +1 -0
- package/dist/chunk-7DWMJCAT.js +453 -0
- package/dist/chunk-7DWMJCAT.js.map +1 -0
- package/dist/chunk-ARRTP33I.js +101 -0
- package/dist/chunk-ARRTP33I.js.map +1 -0
- package/dist/chunk-CPOKFNBG.cjs +101 -0
- package/dist/chunk-CPOKFNBG.cjs.map +1 -0
- package/dist/chunk-G2I4QJOT.cjs +762 -0
- package/dist/chunk-G2I4QJOT.cjs.map +1 -0
- package/dist/{chunk-RU52I7QO.js → chunk-K3R5NG2D.js} +147 -42
- package/dist/chunk-K3R5NG2D.js.map +1 -0
- package/dist/{chunk-DENSCKFC.js → chunk-QBJ22YZR.js} +423 -30
- package/dist/chunk-QBJ22YZR.js.map +1 -0
- package/dist/{chunk-SAZRENEZ.cjs → chunk-RIKBJLS2.cjs} +424 -31
- package/dist/chunk-RIKBJLS2.cjs.map +1 -0
- package/dist/{chunk-6M3BCTBD.cjs → chunk-XDMXXC4Z.cjs} +230 -125
- package/dist/chunk-XDMXXC4Z.cjs.map +1 -0
- package/dist/commands/index.cjs +860 -0
- package/dist/commands/index.cjs.map +1 -0
- package/dist/commands/index.d.cts +592 -0
- package/dist/commands/index.d.ts +592 -0
- package/dist/commands/index.js +860 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/{controller-B7GIybc4.d.cts → controller-pePCJLT1.d.cts} +78 -5
- package/dist/{controller-B7GIybc4.d.ts → controller-pePCJLT1.d.ts} +78 -5
- package/dist/history/index.cjs +12 -2
- package/dist/history/index.cjs.map +1 -1
- package/dist/history/index.d.cts +211 -7
- package/dist/history/index.d.ts +211 -7
- package/dist/history/index.js +13 -3
- package/dist/image-editor/index.cjs +418 -13
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +154 -3
- package/dist/image-editor/index.d.ts +154 -3
- package/dist/image-editor/index.js +417 -12
- package/dist/image-editor/index.js.map +1 -1
- package/dist/index.cjs +34 -76
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -42
- package/dist/index.d.ts +5 -42
- package/dist/index.js +27 -69
- package/dist/index.js.map +1 -1
- package/dist/registry-D5-R90p0.d.cts +209 -0
- package/dist/registry-D5-R90p0.d.ts +209 -0
- package/dist/spreadsheet/index.cjs +19 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +67 -9
- package/dist/spreadsheet/index.d.ts +67 -9
- package/dist/spreadsheet/index.js +18 -2
- package/dist/{types-CJ4U-HHI.d.cts → types-DX4Mh4SL.d.cts} +1 -1
- package/dist/{types-CJ4U-HHI.d.ts → types-DX4Mh4SL.d.ts} +1 -1
- package/dist/types-GT5a5YAX.d.cts +86 -0
- package/dist/types-GT5a5YAX.d.ts +86 -0
- package/package.json +8 -2
- package/dist/chunk-2TWGLC5K.cjs +0 -262
- package/dist/chunk-2TWGLC5K.cjs.map +0 -1
- package/dist/chunk-6M3BCTBD.cjs.map +0 -1
- package/dist/chunk-DENSCKFC.js.map +0 -1
- package/dist/chunk-KAADNAPF.js +0 -262
- package/dist/chunk-KAADNAPF.js.map +0 -1
- package/dist/chunk-RU52I7QO.js.map +0 -1
- package/dist/chunk-SAZRENEZ.cjs.map +0 -1
- package/dist/types-BZgcDsC5.d.cts +0 -42
- package/dist/types-BZgcDsC5.d.ts +0 -42
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/history/HistoryManager.ts","../src/dirty.ts"],"sourcesContent":["// Portions ported from filmcraft-main/crates/engine/src/lib.rs, soundcraft-main/crates/engine/src/lib.rs\n// and photocraft-main/crates/ops/src/lib.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors).\n// See THIRD_PARTY_NOTICES.md.\nimport {\n Command,\n HistoryEntry,\n HistoryManagerOptions,\n HistoryMark,\n HistoryState,\n MergedExecutionOptions,\n SnapshotCommandOptions,\n} from \"./types\";\n\n/** One reversible piece of a step. A gesture step holds one part per distinct command. */\ninterface Part {\n undo: () => void;\n redo: () => void;\n tag?: string;\n bytes: number;\n}\n\n/** One undo step. */\ninterface Entry {\n id: number;\n name: string;\n tag?: string;\n timestamp: number;\n parts: Part[];\n bytes: number;\n}\n\nconst sumBytes = (parts: Part[]) => parts.reduce((n, p) => n + p.bytes, 0);\n\nfunction undoParts(parts: Part[]): void {\n for (let i = parts.length - 1; i >= 0; i--) parts[i].undo();\n}\n\nfunction redoParts(parts: Part[]): void {\n for (let i = 0; i < parts.length; i++) parts[i].redo();\n}\n\n/**\n * UI-agnostic History and Undo/Redo manager supporting command closures,\n * snapshot diffs, transaction batching, high-frequency event coalescing,\n * keyed gestures, undo marks, a memory budget and a History-panel listing.\n */\nexport class HistoryManager {\n private undoStack: Entry[] = [];\n private redoStack: Entry[] = [];\n private readonly maxDepth: number;\n private readonly maxBytes: number;\n private readonly sizeOf?: (command: Command) => number;\n private isApplying = false;\n private batchDepth = 0;\n private currentBatch: Command[] | null = null;\n private listeners: Set<(state: HistoryState) => void> = new Set();\n private onStateChange?: (state: HistoryState) => void;\n private cachedState: HistoryState;\n private cachedEntries: HistoryEntry[] | null = null;\n\n private nextId = 1;\n private revision = 0;\n /** Bumped by `clear()`; marks from an earlier epoch are stale. */\n private epoch = 0;\n /** Number of steps dropped from the bottom (depth / byte budget) so far. */\n private dropped = 0;\n /** Key of the gesture that the top step belongs to (see `executeMerged`). */\n private gestureKey: string | null = null;\n private gestureEntryId: number | null = null;\n /** Key every merged execute takes while a gesture is held (`beginGesture`). */\n private heldKey: string | null = null;\n\n constructor(options: HistoryManagerOptions = {}) {\n this.maxDepth = options.maxDepth ?? 50;\n this.maxBytes = Math.max(0, options.maxBytes ?? 0);\n this.sizeOf = options.sizeOf;\n this.onStateChange = options.onStateChange;\n this.cachedState = this.computeState();\n }\n\n private computeState(): HistoryState {\n const canUndo = this.undoStack.length > 0;\n const canRedo = this.redoStack.length > 0;\n return {\n canUndo,\n canRedo,\n undoName: canUndo ? this.undoStack[this.undoStack.length - 1].name : \"\",\n redoName: canRedo ? this.redoStack[this.redoStack.length - 1].name : \"\",\n undoCount: this.undoStack.length,\n redoCount: this.redoStack.length,\n };\n }\n\n /**\n * Returns a snapshot of the current history state.\n * Cached to maintain referential equality across renders for useSyncExternalStore.\n */\n public getState(): HistoryState {\n return this.cachedState;\n }\n\n /**\n * Monotonic counter bumped on every history change (execute, merge, undo,\n * redo, revert, clear). Useful as a cache key, e.g. for `DirtyTracker`.\n */\n public getRevision(): number {\n return this.revision;\n }\n\n private notify(): void {\n this.revision++;\n this.cachedState = this.computeState();\n this.cachedEntries = null;\n const state = this.cachedState;\n this.onStateChange?.(state);\n this.listeners.forEach((listener) => {\n try {\n listener(state);\n } catch (err) {\n console.error(\"[HistoryManager] Error in state listener:\", err);\n }\n });\n }\n\n private measure(command: Command): number {\n const n = this.sizeOf ? this.sizeOf(command) : (command.byteSize ?? 0);\n return Number.isFinite(n) && n > 0 ? n : 0;\n }\n\n private makeEntry(command: Command, timestamp: number): Entry {\n const part: Part = {\n undo: command.undo,\n redo: command.redo ?? command.execute,\n tag: command.tag,\n bytes: this.measure(command),\n };\n return {\n id: this.nextId++,\n name: command.name,\n tag: command.tag,\n timestamp,\n parts: [part],\n bytes: part.bytes,\n };\n }\n\n /** Pushes a step, clears redo and applies the depth and byte limits. */\n private record(entry: Entry): void {\n this.undoStack.push(entry);\n while (this.undoStack.length > this.maxDepth) {\n this.undoStack.shift();\n this.dropped++;\n }\n this.redoStack = [];\n this.trim();\n }\n\n /**\n * Keeps the stacks within `maxBytes`: the oldest undo steps that do not fit\n * are dropped, but the most recent undo step is always kept.\n */\n private trim(): void {\n if (this.maxBytes <= 0 || this.undoStack.length <= 1) return;\n let total = this.getByteSize();\n while (total > this.maxBytes && this.undoStack.length > 1) {\n total -= this.undoStack.shift()!.bytes;\n this.dropped++;\n }\n }\n\n /** Approximate bytes held by the undo and redo stacks (see `maxBytes`). */\n public getByteSize(): number {\n let total = 0;\n for (const e of this.undoStack) total += e.bytes;\n for (const e of this.redoStack) total += e.bytes;\n return total;\n }\n\n private resetGesture(): void {\n this.gestureKey = null;\n this.gestureEntryId = null;\n }\n\n /**\n * Executes a command and pushes it onto the undo stack.\n * Clears the redo stack and ends the current gesture.\n */\n public execute(command: Command): void {\n if (this.isApplying) {\n command.execute();\n return;\n }\n\n if (this.batchDepth > 0 && this.currentBatch) {\n command.execute();\n this.currentBatch.push({\n ...command,\n timestamp: command.timestamp ?? Date.now(),\n });\n return;\n }\n\n command.execute();\n this.record(this.makeEntry(command, command.timestamp ?? Date.now()));\n this.resetGesture();\n this.notify();\n }\n\n /**\n * Executes a command as part of a continuous interaction (slider scrub, node drag).\n *\n * - With `options.key` (or while a gesture is held with `beginGesture`):\n * consecutive merged executes with the same key fold into one undo step,\n * however long the gesture lasts. Recording any other command, undo, redo,\n * `revertTo`, `goTo`, `clear` or `endGesture()` ends the gesture.\n * - Without a key: coalesces with the previous command if they share the same\n * `tag` and occurred within `windowMs` (default: 300ms).\n */\n public executeMerged(\n command: Command,\n options: MergedExecutionOptions = {},\n ): void {\n const now = Date.now();\n const key = this.heldKey ?? options.key;\n\n if (key !== undefined) {\n if (this.isApplying || this.batchDepth > 0) {\n this.execute({ ...command, timestamp: now });\n return;\n }\n const top = this.undoStack[this.undoStack.length - 1];\n if (top && this.gestureKey === key && this.gestureEntryId === top.id) {\n command.execute();\n const redo = command.redo ?? command.execute;\n const bytes = this.measure(command);\n const last = top.parts[top.parts.length - 1];\n if (last.tag !== undefined && last.tag === command.tag) {\n // Same (absolute) command again: keep the pre-gesture undo, take the new redo.\n top.parts[top.parts.length - 1] = { undo: last.undo, redo, tag: last.tag, bytes };\n } else {\n top.parts.push({ undo: command.undo, redo, tag: command.tag, bytes });\n }\n top.bytes = sumBytes(top.parts);\n top.timestamp = now;\n this.redoStack = [];\n this.trim();\n this.notify();\n return;\n }\n this.execute({ ...command, timestamp: now });\n const pushed = this.undoStack[this.undoStack.length - 1];\n if (pushed) {\n this.gestureKey = key;\n this.gestureEntryId = pushed.id;\n }\n return;\n }\n\n const windowMs = options.windowMs ?? 300;\n\n if (\n command.tag &&\n this.undoStack.length > 0 &&\n this.batchDepth === 0 &&\n !this.isApplying\n ) {\n const top = this.undoStack[this.undoStack.length - 1];\n const elapsed = top.timestamp ? now - top.timestamp : Infinity;\n\n if (top.tag === command.tag && elapsed <= windowMs) {\n command.execute();\n // Replace top with merged command keeping the original initial undo\n const initialParts = top.parts;\n const bytes = this.measure(command);\n this.undoStack[this.undoStack.length - 1] = {\n id: top.id,\n name: command.name,\n tag: command.tag,\n timestamp: now,\n parts: [\n {\n undo: () => undoParts(initialParts),\n redo: command.redo ?? command.execute,\n tag: command.tag,\n bytes,\n },\n ],\n bytes,\n };\n this.redoStack = [];\n this.resetGesture();\n this.trim();\n this.notify();\n return;\n }\n }\n\n this.execute({ ...command, timestamp: now });\n }\n\n /**\n * Holds a gesture: until `endGesture()`, every `executeMerged` call (whatever\n * its own key or tag) joins one undo step, e.g. an automation pass and\n * whatever is dragged during it. Commands recorded with `execute` still end\n * the current step; the next merged execute then starts a new one.\n */\n public beginGesture(key: string): void {\n if (this.heldKey === key) return;\n this.heldKey = key;\n this.resetGesture();\n }\n\n /** Ends the current (keyed or held) gesture; the next merged execute starts a new step. */\n public endGesture(): void {\n this.heldKey = null;\n this.resetGesture();\n }\n\n /** The key of the gesture currently merging into the top step, if any. */\n public getGestureKey(): string | null {\n return this.heldKey ?? this.gestureKey;\n }\n\n /**\n * Batches multiple commands executed within `fn` into a single atomic undo entry.\n */\n public batch(name: string, fn: () => void): void {\n if (this.isApplying) {\n fn();\n return;\n }\n\n if (this.batchDepth > 0) {\n // Already inside a batch; execute directly\n fn();\n return;\n }\n\n this.batchDepth++;\n this.currentBatch = [];\n\n try {\n fn();\n } catch (error) {\n const executed = this.currentBatch ?? [];\n this.batchDepth--;\n this.currentBatch = null;\n this.rollback(executed);\n throw error;\n }\n\n this.batchDepth--;\n const batchCommands: Command[] = this.currentBatch ?? [];\n this.currentBatch = null;\n\n if (batchCommands.length > 0) {\n const run = () => {\n for (let i = 0; i < batchCommands.length; i++) {\n (batchCommands[i].redo ?? batchCommands[i].execute)();\n }\n };\n const bytes = batchCommands.reduce((n, c) => n + this.measure(c), 0);\n this.record({\n id: this.nextId++,\n name,\n timestamp: Date.now(),\n parts: [\n {\n undo: () => {\n for (let i = batchCommands.length - 1; i >= 0; i--) {\n batchCommands[i].undo();\n }\n },\n redo: run,\n bytes,\n },\n ],\n bytes,\n });\n this.resetGesture();\n this.notify();\n }\n }\n\n /**\n * Reverts already-executed commands of an aborted batch, newest first.\n * Commands executed by the undo closures themselves are not recorded.\n */\n private rollback(commands: Command[]): void {\n const wasApplying = this.isApplying;\n this.isApplying = true;\n try {\n for (let i = commands.length - 1; i >= 0; i--) {\n try {\n commands[i].undo();\n } catch (err) {\n console.error(\n `[HistoryManager] Error rolling back \"${commands[i].name}\" of a failed batch:`,\n err,\n );\n }\n }\n } finally {\n this.isApplying = wasApplying;\n }\n }\n\n /** Undoes the top step (caller holds `isApplying`). Throws leave it on the stack. */\n private undoOne(): void {\n const entry = this.undoStack[this.undoStack.length - 1];\n undoParts(entry.parts);\n this.undoStack.pop();\n this.redoStack.push(entry);\n }\n\n private redoOne(): void {\n const entry = this.redoStack[this.redoStack.length - 1];\n redoParts(entry.parts);\n this.redoStack.pop();\n this.undoStack.push(entry);\n }\n\n /**\n * Reverts the most recent command on the undo stack.\n * Returns true if a command was undone, false otherwise.\n */\n public undo(): boolean {\n if (this.undoStack.length === 0 || this.isApplying || this.batchDepth > 0) {\n return false;\n }\n\n this.isApplying = true;\n this.resetGesture();\n try {\n // If undo throws, the command stays on the undo stack and the error propagates.\n this.undoOne();\n return true;\n } finally {\n this.isApplying = false;\n this.notify();\n }\n }\n\n /**\n * Re-applies the most recent undone command on the redo stack.\n * Returns true if a command was redone, false otherwise.\n */\n public redo(): boolean {\n if (this.redoStack.length === 0 || this.isApplying || this.batchDepth > 0) {\n return false;\n }\n\n this.isApplying = true;\n this.resetGesture();\n try {\n // If redo throws, the command stays on the redo stack and the error propagates.\n this.redoOne();\n return true;\n } finally {\n this.isApplying = false;\n this.notify();\n }\n }\n\n /**\n * Captures the current point in the history (its top step), for `revertTo`.\n */\n public mark(): HistoryMark {\n const top = this.undoStack[this.undoStack.length - 1];\n return {\n topId: top ? top.id : null,\n nextId: this.nextId,\n epoch: this.epoch,\n dropped: this.dropped,\n };\n }\n\n /** Whether `mark` still points into the undo history. */\n public isMarkValid(mark: HistoryMark): boolean {\n if (mark.epoch !== this.epoch) return false;\n if (mark.topId === null) return mark.dropped === this.dropped;\n return this.undoStack.some((e) => e.id === mark.topId);\n }\n\n /**\n * Takes back every step made since `mark` (merged or not), e.g. to cancel a\n * drag with Escape: the state returns to how it was at `mark`, and neither\n * undo nor redo keeps the reverted steps. Does nothing when `mark` has left\n * the history (undone past, dropped by the limits, or cleared).\n * Returns how many steps were taken back.\n */\n public revertTo(mark: HistoryMark): number {\n if (this.isApplying || this.batchDepth > 0 || !this.isMarkValid(mark)) {\n return 0;\n }\n const redoBefore = this.redoStack.length;\n let n = 0;\n this.isApplying = true;\n this.resetGesture();\n try {\n while (this.undoStack.length > 0) {\n const top = this.undoStack[this.undoStack.length - 1];\n if (top.id === mark.topId) break;\n undoParts(top.parts);\n this.undoStack.pop();\n n++;\n }\n } finally {\n this.isApplying = false;\n // Steps made after the mark are gone for good, including undone ones.\n this.redoStack =\n n > 0 ? [] : this.redoStack.filter((e) => e.id < mark.nextId);\n if (n > 0 || this.redoStack.length !== redoBefore) this.notify();\n }\n return n;\n }\n\n /**\n * Lists the steps for a History panel: undo steps oldest first, then redo\n * steps (the next redo first). Cached until the history changes.\n */\n public getEntries(): HistoryEntry[] {\n if (this.cachedEntries) return this.cachedEntries;\n const u = this.undoStack.length;\n const entries: HistoryEntry[] = this.undoStack.map((e, index) => ({\n id: e.id,\n label: e.name,\n index,\n current: index === u - 1,\n undone: false,\n }));\n for (let i = this.redoStack.length - 1; i >= 0; i--) {\n const e = this.redoStack[i];\n entries.push({\n id: e.id,\n label: e.name,\n index: entries.length,\n current: false,\n undone: true,\n });\n }\n this.cachedEntries = entries;\n return entries;\n }\n\n /**\n * Undoes or redoes until the step at `index` (from `getEntries`) is the\n * current one; `-1` undoes everything. Returns how many steps were applied.\n */\n public goTo(index: number): number {\n if (this.isApplying || this.batchDepth > 0) return 0;\n const total = this.undoStack.length + this.redoStack.length;\n const target = Math.max(0, Math.min(total, Math.floor(index) + 1));\n if (target === this.undoStack.length) return 0;\n let n = 0;\n this.isApplying = true;\n this.resetGesture();\n try {\n while (this.undoStack.length > target) {\n this.undoOne();\n n++;\n }\n while (this.undoStack.length < target) {\n this.redoOne();\n n++;\n }\n } finally {\n this.isApplying = false;\n this.notify();\n }\n return n;\n }\n\n /**\n * Clears both undo and redo stacks.\n */\n public clear(): void {\n this.undoStack = [];\n this.redoStack = [];\n this.epoch++;\n this.resetGesture();\n this.notify();\n }\n\n /**\n * Subscribes to history state changes.\n * The listener is immediately invoked with the current state.\n * Returns an unsubscribe function.\n */\n public subscribe(listener: (state: HistoryState) => void): () => void {\n this.listeners.add(listener);\n listener(this.getState());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Utility helper to create a Command from before/after state snapshots.\n */\n public static createSnapshotCommand<T>(\n options: SnapshotCommandOptions<T>,\n ): Command {\n return {\n name: options.name,\n tag: options.tag,\n execute: () => options.apply(options.after),\n undo: () => options.apply(options.before),\n redo: () => options.apply(options.after),\n };\n }\n}\n\n/**\n * Creates and initializes a new HistoryManager instance.\n */\nexport function createHistoryManager(\n options: HistoryManagerOptions = {},\n): HistoryManager {\n return new HistoryManager(options);\n}\n","// Portions ported from soundcraft-main/crates/engine/src/lib.rs (MIT OR Apache-2.0,\n// © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Compares two values for structural equality.\n * Handles primitives, objects, arrays, and null/undefined.\n */\nexport function defaultIsEqual<T>(a: T, b: T): boolean {\n if (Object.is(a, b)) return true;\n if (\n typeof a !== \"object\" ||\n a === null ||\n typeof b !== \"object\" ||\n b === null\n ) {\n return false;\n }\n\n // Fast path for arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!defaultIsEqual(a[i], b[i])) return false;\n }\n return true;\n }\n if (Array.isArray(a) !== Array.isArray(b)) {\n return false;\n }\n\n const keysA = Object.keys(a) as (keyof T)[];\n const keysB = Object.keys(b) as (keyof T)[];\n\n if (keysA.length !== keysB.length) return false;\n\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) return false;\n if (!defaultIsEqual(a[key], b[key])) return false;\n }\n\n return true;\n}\n\nexport interface DirtyTrackerOptions<T> {\n /**\n * Equality check. When `normalize` is set it receives the normalized values.\n */\n isEqual?: (a: T, b: T) => boolean;\n /**\n * Projects a value onto the part that counts as \"the document\" before\n * comparing, e.g. `({ selection, viewport, ...doc }) => doc` so that\n * selecting, scrolling or zooming never marks the document dirty.\n */\n normalize?: (value: T) => unknown;\n}\n\ntype Thenable = { then: (...args: any[]) => any };\n\nconst isThenable = (value: unknown): value is Thenable =>\n !!value &&\n (typeof value === \"object\" || typeof value === \"function\") &&\n typeof (value as Thenable).then === \"function\";\n\n/**\n * Tracks a baseline (saved) value and determines whether the current value is\n * dirty (modified).\n *\n * Stateless use: `isDirty(current)`. Stateful use: feed values with\n * `update(current, revision?)`, read `dirty`, `subscribe` to changes, call\n * `markClean()` after saving and wrap programmatic loads in `suppress(fn)`.\n */\nexport class DirtyTracker<T = unknown> {\n private baseline: T;\n private readonly isEqual: (a: T, b: T) => boolean;\n private readonly normalize?: (value: T) => unknown;\n private normalizedBaseline: { value: unknown } | null = null;\n /** `isDirty` result as of a revision (and baseline version). */\n private cache: { revision: number; baseline: number; dirty: boolean } | null =\n null;\n private baselineVersion = 0;\n private suppressDepth = 0;\n private hasValue = false;\n private lastValue: T | undefined;\n private dirtyState = false;\n private listeners = new Set<(dirty: boolean) => void>();\n\n constructor(\n initialBaseline: T,\n options?: DirtyTrackerOptions<T> | ((a: T, b: T) => boolean),\n ) {\n this.baseline = initialBaseline;\n if (typeof options === \"function\") {\n this.isEqual = options;\n } else {\n this.isEqual = options?.isEqual ?? defaultIsEqual;\n this.normalize = options?.normalize;\n }\n }\n\n /**\n * Returns current baseline value.\n */\n public getBaseline(): T {\n return this.baseline;\n }\n\n private changeBaseline(value: T): void {\n this.baseline = value;\n this.baselineVersion++;\n this.normalizedBaseline = null;\n this.cache = null;\n }\n\n /**\n * Sets a new baseline value without triggering any dirty state.\n * If a value is being tracked (`update`), `dirty` is recomputed against it.\n */\n public setBaseline(value: T): void {\n this.changeBaseline(value);\n if (this.hasValue) {\n this.setDirtyState(this.compare(this.lastValue as T));\n }\n }\n\n private compare(current: T): boolean {\n if (!this.normalize) return !this.isEqual(this.baseline, current);\n this.normalizedBaseline ??= { value: this.normalize(this.baseline) };\n return !this.isEqual(\n this.normalizedBaseline.value as T,\n this.normalize(current) as T,\n );\n }\n\n /**\n * Checks whether the given value differs from the baseline.\n * With a `revision` (e.g. `HistoryManager.getRevision()`), the result is\n * cached and reused for the same revision until the baseline changes.\n */\n public isDirty(current: T, revision?: number): boolean {\n if (revision === undefined) return this.compare(current);\n const cache = this.cache;\n if (\n cache &&\n cache.revision === revision &&\n cache.baseline === this.baselineVersion\n ) {\n return cache.dirty;\n }\n const dirty = this.compare(current);\n this.cache = { revision, baseline: this.baselineVersion, dirty };\n return dirty;\n }\n\n /**\n * Resets the baseline to the provided current value (or clears dirty status).\n */\n public reset(current: T): void {\n this.changeBaseline(current);\n this.hasValue = true;\n this.lastValue = current;\n this.setDirtyState(false);\n }\n\n /**\n * Records the current value and returns whether it is dirty. While\n * suppressed (`suppress`), the value becomes the new baseline instead, so\n * programmatic loads and restores never mark the document dirty.\n */\n public update(current: T, revision?: number): boolean {\n this.hasValue = true;\n this.lastValue = current;\n if (this.suppressDepth > 0) {\n this.changeBaseline(current);\n this.setDirtyState(false);\n return false;\n }\n const dirty = this.isDirty(current, revision);\n this.setDirtyState(dirty);\n return dirty;\n }\n\n /** Whether the last value passed to `update` differs from the baseline. */\n public get dirty(): boolean {\n return this.dirtyState;\n }\n\n /** Whether `suppress` is currently running. */\n public get suppressed(): boolean {\n return this.suppressDepth > 0;\n }\n\n /**\n * Marks the document saved: `current` (default: the last value passed to\n * `update`) becomes the baseline.\n */\n public markClean(...args: [] | [current: T]): void {\n if (args.length > 0) {\n this.reset(args[0] as T);\n } else if (this.hasValue) {\n this.reset(this.lastValue as T);\n } else {\n this.setDirtyState(false);\n }\n }\n\n /**\n * Runs `fn` without marking anything dirty: every `update` during it\n * rebaselines. Re-entrant (depth counter). If `fn` returns a promise, the\n * suppression lasts until it settles.\n */\n public suppress<R>(fn: () => R): R {\n this.suppressDepth++;\n let result: R;\n try {\n result = fn();\n } catch (error) {\n this.suppressDepth--;\n throw error;\n }\n if (isThenable(result)) {\n const release = () => {\n this.suppressDepth--;\n };\n return (result as unknown as Promise<unknown>).then(\n (value) => {\n release();\n return value;\n },\n (error) => {\n release();\n throw error;\n },\n ) as unknown as R;\n }\n this.suppressDepth--;\n return result;\n }\n\n /**\n * Subscribes to changes of `dirty`. The listener is called immediately with\n * the current value. Returns an unsubscribe function.\n */\n public subscribe(listener: (dirty: boolean) => void): () => void {\n this.listeners.add(listener);\n listener(this.dirtyState);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private setDirtyState(dirty: boolean): void {\n if (dirty === this.dirtyState) return;\n this.dirtyState = dirty;\n this.listeners.forEach((listener) => {\n try {\n listener(dirty);\n } catch (err) {\n console.error(\"[DirtyTracker] Error in listener:\", err);\n }\n });\n }\n}\n\n/**\n * Creates a DirtyTracker instance.\n */\nexport function createDirtyTracker<T>(\n initialBaseline: T,\n options?: DirtyTrackerOptions<T> | ((a: T, b: T) => boolean),\n): DirtyTracker<T> {\n return new DirtyTracker(initialBaseline, options);\n}\n\n/**\n * Runs `fn` with dirty tracking suppressed (see `DirtyTracker.suppress`).\n */\nexport function withoutDirty<R>(tracker: DirtyTracker<any>, fn: () => R): R {\n return tracker.suppress(fn);\n}\n\n/** Minimal history surface used by `bindDirtyToHistory` (`HistoryManager` satisfies it). */\nexport interface DirtyHistorySource {\n subscribe(listener: (...args: any[]) => void): () => void;\n getRevision?(): number;\n}\n\nexport interface BindDirtyToHistoryOptions<T> extends DirtyTrackerOptions<T> {\n history: DirtyHistorySource;\n /** Reads the current document value. */\n getValue: () => T;\n /** Existing tracker to drive (default: a new one baselined on `getValue()`). */\n tracker?: DirtyTracker<T>;\n}\n\nexport interface HistoryDirtyBinding<T> {\n readonly tracker: DirtyTracker<T>;\n /** Whether the document differs from the saved baseline. */\n readonly dirty: boolean;\n /** Marks the current value as saved. */\n markClean(): void;\n /** Runs `fn` (e.g. a load or restore) without marking the document dirty. */\n suppress<R>(fn: () => R): R;\n /** Re-reads the value after a change made outside the history. */\n refresh(): boolean;\n subscribe(listener: (dirty: boolean) => void): () => void;\n /** Stops listening to the history. */\n destroy(): void;\n}\n\n/**\n * Keeps a `DirtyTracker` in step with a `HistoryManager`: every history change\n * re-reads the value (cached per history revision), so undoing back to the\n * saved state makes the document clean again.\n */\nexport function bindDirtyToHistory<T>(\n options: BindDirtyToHistoryOptions<T>,\n): HistoryDirtyBinding<T> {\n const { history, getValue } = options;\n const tracker =\n options.tracker ??\n new DirtyTracker<T>(getValue(), {\n isEqual: options.isEqual,\n normalize: options.normalize,\n });\n const unsubscribe = history.subscribe(() => {\n tracker.update(getValue(), history.getRevision?.());\n });\n return {\n tracker,\n get dirty() {\n return tracker.dirty;\n },\n markClean: () => tracker.markClean(getValue()),\n suppress: <R>(fn: () => R): R => {\n // The loaded value becomes the baseline even if the load never touched the history.\n const rebaseline = () => {\n if (!tracker.suppressed) tracker.reset(getValue());\n };\n const result = tracker.suppress(fn);\n if (isThenable(result)) {\n return (result as unknown as Promise<unknown>).then((value) => {\n rebaseline();\n return value;\n }) as unknown as R;\n }\n rebaseline();\n return result;\n },\n refresh: () => tracker.update(getValue()),\n subscribe: (listener) => tracker.subscribe(listener),\n destroy: unsubscribe,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// src/commands/chords.ts
|
|
2
|
+
var PLATFORMS = ["mac", "windows", "linux"];
|
|
3
|
+
function detectPlatform() {
|
|
4
|
+
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
5
|
+
const text = `${nav?.platform ?? ""} ${nav?.userAgent ?? ""}`;
|
|
6
|
+
if (/Mac|iPod|iPhone|iPad/.test(nav?.platform || nav?.userAgent || "")) return "mac";
|
|
7
|
+
if (/Win/.test(text)) return "windows";
|
|
8
|
+
return "linux";
|
|
9
|
+
}
|
|
10
|
+
var NAMED_KEYS = {
|
|
11
|
+
"[": "BracketLeft",
|
|
12
|
+
"]": "BracketRight",
|
|
13
|
+
"-": "Minus",
|
|
14
|
+
"=": "Equal",
|
|
15
|
+
plus: "Equal",
|
|
16
|
+
",": "Comma",
|
|
17
|
+
".": "Period",
|
|
18
|
+
"/": "Slash",
|
|
19
|
+
"\\": "Backslash",
|
|
20
|
+
";": "Semicolon",
|
|
21
|
+
"'": "Quote",
|
|
22
|
+
"`": "Backquote",
|
|
23
|
+
enter: "Enter",
|
|
24
|
+
return: "Enter",
|
|
25
|
+
"\u21A9": "Enter",
|
|
26
|
+
escape: "Escape",
|
|
27
|
+
esc: "Escape",
|
|
28
|
+
"\u238B": "Escape",
|
|
29
|
+
space: "Space",
|
|
30
|
+
spacebar: "Space",
|
|
31
|
+
" ": "Space",
|
|
32
|
+
tab: "Tab",
|
|
33
|
+
"\u21E5": "Tab",
|
|
34
|
+
backspace: "Backspace",
|
|
35
|
+
"\u232B": "Backspace",
|
|
36
|
+
delete: "Delete",
|
|
37
|
+
del: "Delete",
|
|
38
|
+
"\u2326": "Delete",
|
|
39
|
+
insert: "Insert",
|
|
40
|
+
home: "Home",
|
|
41
|
+
end: "End",
|
|
42
|
+
pageup: "PageUp",
|
|
43
|
+
pgup: "PageUp",
|
|
44
|
+
pagedown: "PageDown",
|
|
45
|
+
pgdn: "PageDown",
|
|
46
|
+
up: "ArrowUp",
|
|
47
|
+
arrowup: "ArrowUp",
|
|
48
|
+
"\u2191": "ArrowUp",
|
|
49
|
+
down: "ArrowDown",
|
|
50
|
+
arrowdown: "ArrowDown",
|
|
51
|
+
"\u2193": "ArrowDown",
|
|
52
|
+
left: "ArrowLeft",
|
|
53
|
+
arrowleft: "ArrowLeft",
|
|
54
|
+
"\u2190": "ArrowLeft",
|
|
55
|
+
right: "ArrowRight",
|
|
56
|
+
arrowright: "ArrowRight",
|
|
57
|
+
"\u2192": "ArrowRight",
|
|
58
|
+
bracketleft: "BracketLeft",
|
|
59
|
+
bracketright: "BracketRight",
|
|
60
|
+
minus: "Minus",
|
|
61
|
+
equal: "Equal",
|
|
62
|
+
comma: "Comma",
|
|
63
|
+
period: "Period",
|
|
64
|
+
slash: "Slash",
|
|
65
|
+
backslash: "Backslash",
|
|
66
|
+
semicolon: "Semicolon",
|
|
67
|
+
quote: "Quote",
|
|
68
|
+
backquote: "Backquote"
|
|
69
|
+
};
|
|
70
|
+
var CODE_TEXT = {
|
|
71
|
+
BracketLeft: "[",
|
|
72
|
+
BracketRight: "]",
|
|
73
|
+
Minus: "-",
|
|
74
|
+
Equal: "=",
|
|
75
|
+
Comma: ",",
|
|
76
|
+
Period: ".",
|
|
77
|
+
Slash: "/",
|
|
78
|
+
Backslash: "\\",
|
|
79
|
+
Semicolon: ";",
|
|
80
|
+
Quote: "'",
|
|
81
|
+
Backquote: "`",
|
|
82
|
+
ArrowUp: "Up",
|
|
83
|
+
ArrowDown: "Down",
|
|
84
|
+
ArrowLeft: "Left",
|
|
85
|
+
ArrowRight: "Right"
|
|
86
|
+
};
|
|
87
|
+
var MAC_KEY_GLYPHS = {
|
|
88
|
+
Backspace: "\u232B",
|
|
89
|
+
Delete: "\u2326",
|
|
90
|
+
Enter: "\u21A9",
|
|
91
|
+
Escape: "\u238B",
|
|
92
|
+
Tab: "\u21E5",
|
|
93
|
+
ArrowUp: "\u2191",
|
|
94
|
+
ArrowDown: "\u2193",
|
|
95
|
+
ArrowLeft: "\u2190",
|
|
96
|
+
ArrowRight: "\u2192",
|
|
97
|
+
PageUp: "Page Up",
|
|
98
|
+
PageDown: "Page Down",
|
|
99
|
+
Space: "Space"
|
|
100
|
+
};
|
|
101
|
+
var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
102
|
+
...Object.values(NAMED_KEYS),
|
|
103
|
+
"Enter",
|
|
104
|
+
"Escape",
|
|
105
|
+
"Space",
|
|
106
|
+
"Tab",
|
|
107
|
+
"Backspace",
|
|
108
|
+
"Delete",
|
|
109
|
+
"Insert",
|
|
110
|
+
"Home",
|
|
111
|
+
"End",
|
|
112
|
+
"PageUp",
|
|
113
|
+
"PageDown"
|
|
114
|
+
]);
|
|
115
|
+
function keyToCode(token) {
|
|
116
|
+
if (!token) return null;
|
|
117
|
+
if (/^[a-z]$/i.test(token)) return `Key${token.toUpperCase()}`;
|
|
118
|
+
if (/^[0-9]$/.test(token)) return `Digit${token}`;
|
|
119
|
+
if (/^f([1-9]|1[0-9]|2[0-4])$/i.test(token)) return token.toUpperCase();
|
|
120
|
+
if (/^Key[A-Z]$/.test(token) || /^Digit[0-9]$/.test(token) || /^Numpad\w+$/.test(token)) return token;
|
|
121
|
+
if (KNOWN_CODES.has(token)) return token;
|
|
122
|
+
return NAMED_KEYS[token] ?? NAMED_KEYS[token.toLowerCase().replace(/[\s_]/g, "")] ?? null;
|
|
123
|
+
}
|
|
124
|
+
function codeToKeyText(code) {
|
|
125
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3);
|
|
126
|
+
if (/^Digit[0-9]$/.test(code)) return code.slice(5);
|
|
127
|
+
return CODE_TEXT[code] ?? code;
|
|
128
|
+
}
|
|
129
|
+
var GLYPH_MODIFIERS = { "\u2318": "Meta+", "\u2303": "Ctrl+", "\u2325": "Alt+", "\u21E7": "Shift+" };
|
|
130
|
+
function parseChord(text) {
|
|
131
|
+
const source = text.trim();
|
|
132
|
+
if (!source) throw new Error("Empty shortcut.");
|
|
133
|
+
let expanded = "";
|
|
134
|
+
for (const ch of source) expanded += GLYPH_MODIFIERS[ch] ?? ch;
|
|
135
|
+
const parts = expanded.split("+").map((p) => p.trim()).filter(Boolean);
|
|
136
|
+
if (parts.length === 0) throw new Error(`"${text}" has no key.`);
|
|
137
|
+
const chord = { key: "" };
|
|
138
|
+
parts.forEach((part, index) => {
|
|
139
|
+
const low = part.toLowerCase();
|
|
140
|
+
const isLast = index === parts.length - 1;
|
|
141
|
+
if (low === "mod" || low === "cmdorctrl" || low === "commandorcontrol" || low === "primary") chord.mod = true;
|
|
142
|
+
else if (low === "ctrl" || low === "control" || low === "ctl") chord.ctrl = true;
|
|
143
|
+
else if (low === "meta" || low === "cmd" || low === "command" || low === "super" || low === "win") chord.meta = true;
|
|
144
|
+
else if (low === "alt" || low === "option" || low === "opt") chord.alt = true;
|
|
145
|
+
else if (low === "shift") chord.shift = true;
|
|
146
|
+
else if (isLast) {
|
|
147
|
+
const code = keyToCode(part);
|
|
148
|
+
if (!code) throw new Error(`Unknown key "${part}" in shortcut "${text}".`);
|
|
149
|
+
chord.key = code;
|
|
150
|
+
} else throw new Error(`Unknown modifier "${part}" in shortcut "${text}".`);
|
|
151
|
+
});
|
|
152
|
+
if (!chord.key) throw new Error(`"${text}" has no key.`);
|
|
153
|
+
return chord;
|
|
154
|
+
}
|
|
155
|
+
function chordToString(chord) {
|
|
156
|
+
const parts = [];
|
|
157
|
+
if (chord.mod) parts.push("Mod");
|
|
158
|
+
if (chord.ctrl) parts.push("Ctrl");
|
|
159
|
+
if (chord.meta) parts.push("Meta");
|
|
160
|
+
if (chord.alt) parts.push("Alt");
|
|
161
|
+
if (chord.shift) parts.push("Shift");
|
|
162
|
+
parts.push(codeToKeyText(chord.key));
|
|
163
|
+
return parts.join("+");
|
|
164
|
+
}
|
|
165
|
+
function normalizeChord(text) {
|
|
166
|
+
return chordToString(parseChord(text));
|
|
167
|
+
}
|
|
168
|
+
var toChord = (chord) => typeof chord === "string" ? parseChord(chord) : chord;
|
|
169
|
+
function effectiveChord(chord, platform) {
|
|
170
|
+
const c = toChord(chord);
|
|
171
|
+
const mac = platform === "mac";
|
|
172
|
+
return {
|
|
173
|
+
key: c.key,
|
|
174
|
+
ctrl: !!c.ctrl || !mac && !!c.mod,
|
|
175
|
+
meta: !!c.meta || mac && !!c.mod,
|
|
176
|
+
alt: !!c.alt,
|
|
177
|
+
shift: !!c.shift
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function effectiveChordKey(chord, platform) {
|
|
181
|
+
const e = effectiveChord(chord, platform);
|
|
182
|
+
return [e.ctrl && "Ctrl", e.meta && "Meta", e.alt && "Alt", e.shift && "Shift", e.key].filter(Boolean).join("+");
|
|
183
|
+
}
|
|
184
|
+
function formatShortcut(chord, platform = detectPlatform()) {
|
|
185
|
+
let c;
|
|
186
|
+
try {
|
|
187
|
+
c = toChord(chord);
|
|
188
|
+
} catch {
|
|
189
|
+
return String(chord);
|
|
190
|
+
}
|
|
191
|
+
const e = effectiveChord(c, platform);
|
|
192
|
+
if (platform === "mac") {
|
|
193
|
+
const mods = `${e.ctrl ? "\u2303" : ""}${e.alt ? "\u2325" : ""}${e.shift ? "\u21E7" : ""}${e.meta ? "\u2318" : ""}`;
|
|
194
|
+
return mods + (MAC_KEY_GLYPHS[c.key] ?? codeToKeyText(c.key));
|
|
195
|
+
}
|
|
196
|
+
const parts = [];
|
|
197
|
+
if (e.ctrl) parts.push("Ctrl");
|
|
198
|
+
if (e.meta) parts.push(platform === "windows" ? "Win" : "Super");
|
|
199
|
+
if (e.alt) parts.push("Alt");
|
|
200
|
+
if (e.shift) parts.push("Shift");
|
|
201
|
+
const key = c.key === "PageUp" ? "Page Up" : c.key === "PageDown" ? "Page Down" : codeToKeyText(c.key);
|
|
202
|
+
parts.push(key);
|
|
203
|
+
return parts.join("+");
|
|
204
|
+
}
|
|
205
|
+
function eventCode(event) {
|
|
206
|
+
if (event.code) return event.code;
|
|
207
|
+
return codeFromKey(event.key ?? "") ?? event.key ?? "";
|
|
208
|
+
}
|
|
209
|
+
function shortcutCode(event) {
|
|
210
|
+
const code = eventCode(event);
|
|
211
|
+
const key = event.key ?? "";
|
|
212
|
+
if (!event.altKey && /^Key[A-Z]$/.test(code) && /^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;
|
|
213
|
+
return code;
|
|
214
|
+
}
|
|
215
|
+
function codeFromKey(key) {
|
|
216
|
+
if (!key) return null;
|
|
217
|
+
if (key.length === 1) return /^[a-z0-9]$/i.test(key) || key in NAMED_KEYS ? keyToCode(key) : null;
|
|
218
|
+
return keyToCode(key);
|
|
219
|
+
}
|
|
220
|
+
function isEditableEventTarget(event) {
|
|
221
|
+
let target = event.target ?? null;
|
|
222
|
+
if (!target) target = typeof document !== "undefined" ? document.activeElement : null;
|
|
223
|
+
if (!target || typeof target.tagName !== "string") return false;
|
|
224
|
+
const tag = target.tagName.toUpperCase();
|
|
225
|
+
return !!(tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable || target.getAttribute?.("contenteditable") === "true" || target.closest?.('[contenteditable="true"]') || target.closest?.(".ProseMirror"));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/commands/shortcuts.ts
|
|
229
|
+
function toShortcutBindings(command, input, scope) {
|
|
230
|
+
if (input === void 0) return [];
|
|
231
|
+
const list = Array.isArray(input) ? input : [input];
|
|
232
|
+
return list.map(
|
|
233
|
+
(item) => typeof item === "string" ? { command, chord: item, ...scope ? { scope } : {} } : { ...item, command, ...item.scope ?? scope ? { scope: item.scope ?? scope } : {} }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
var asList = (scopes) => scopes === void 0 ? [] : typeof scopes === "string" ? [scopes] : scopes;
|
|
237
|
+
var ShortcutMap = class {
|
|
238
|
+
defaults = [];
|
|
239
|
+
overrides = /* @__PURE__ */ new Map();
|
|
240
|
+
listeners = /* @__PURE__ */ new Set();
|
|
241
|
+
options;
|
|
242
|
+
constructor(bindings = [], options = {}) {
|
|
243
|
+
this.options = options;
|
|
244
|
+
for (const binding of bindings) this.defaults.push(this.entry(binding, "default"));
|
|
245
|
+
}
|
|
246
|
+
get platform() {
|
|
247
|
+
return this.options.platform ?? detectPlatform();
|
|
248
|
+
}
|
|
249
|
+
entry(binding, source) {
|
|
250
|
+
return { binding, chord: typeof binding.chord === "string" ? parseChord(binding.chord) : binding.chord, source };
|
|
251
|
+
}
|
|
252
|
+
changed() {
|
|
253
|
+
for (const listener of Array.from(this.listeners)) listener();
|
|
254
|
+
}
|
|
255
|
+
/** Subscribes to binding changes. Returns an unsubscribe function. */
|
|
256
|
+
subscribe(listener) {
|
|
257
|
+
this.listeners.add(listener);
|
|
258
|
+
return () => {
|
|
259
|
+
this.listeners.delete(listener);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
/** Adds default bindings (throws on invalid chord text). Returns a function removing them. */
|
|
263
|
+
bind(bindings) {
|
|
264
|
+
const added = (Array.isArray(bindings) ? bindings : [bindings]).map((b) => this.entry(b, "default"));
|
|
265
|
+
if (added.length === 0) return () => {
|
|
266
|
+
};
|
|
267
|
+
this.defaults.push(...added);
|
|
268
|
+
this.changed();
|
|
269
|
+
return () => {
|
|
270
|
+
const before = this.defaults.length;
|
|
271
|
+
this.defaults = this.defaults.filter((e) => !added.includes(e));
|
|
272
|
+
if (this.defaults.length !== before) this.changed();
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* User override for a command: replaces all of its default bindings with `chords`
|
|
277
|
+
* (an empty list unbinds it). Throws on invalid chord text.
|
|
278
|
+
*/
|
|
279
|
+
setOverride(command, chords) {
|
|
280
|
+
const list = Array.isArray(chords) ? chords : [chords];
|
|
281
|
+
const entries = list.map(
|
|
282
|
+
(item) => this.entry(
|
|
283
|
+
typeof item === "string" || !("chord" in item) ? { command, chord: item } : { ...item, command },
|
|
284
|
+
"override"
|
|
285
|
+
)
|
|
286
|
+
);
|
|
287
|
+
this.overrides.set(command, entries);
|
|
288
|
+
this.changed();
|
|
289
|
+
}
|
|
290
|
+
/** Removes a user override (the defaults apply again). */
|
|
291
|
+
clearOverride(command) {
|
|
292
|
+
if (this.overrides.delete(command)) this.changed();
|
|
293
|
+
}
|
|
294
|
+
/** User overrides as plain data (`{ command: ['Mod+K'] }`), e.g. to persist them. */
|
|
295
|
+
getOverrides() {
|
|
296
|
+
const out = {};
|
|
297
|
+
for (const [command, entries] of this.overrides) {
|
|
298
|
+
out[command] = entries.map((e) => chordToString(e.chord));
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
/** Restores overrides saved with `getOverrides` (replacing the current ones). */
|
|
303
|
+
setOverrides(overrides) {
|
|
304
|
+
this.overrides.clear();
|
|
305
|
+
for (const [command, chords] of Object.entries(overrides)) {
|
|
306
|
+
this.overrides.set(
|
|
307
|
+
command,
|
|
308
|
+
chords.map((chord) => this.entry({ command, chord }, "override"))
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
this.changed();
|
|
312
|
+
}
|
|
313
|
+
active(platform) {
|
|
314
|
+
const out = [];
|
|
315
|
+
for (const e of this.defaults) {
|
|
316
|
+
if (!this.overrides.has(e.binding.command)) out.push(e);
|
|
317
|
+
}
|
|
318
|
+
for (const entries of this.overrides.values()) out.push(...entries);
|
|
319
|
+
return out.filter((e) => !e.binding.platforms || e.binding.platforms.includes(platform));
|
|
320
|
+
}
|
|
321
|
+
/** Effective bindings on `platform` (defaults without overridden commands, plus overrides). */
|
|
322
|
+
bindings(platform = this.platform) {
|
|
323
|
+
return this.active(platform).map((e) => e.binding);
|
|
324
|
+
}
|
|
325
|
+
/** Canonical chord text bound to `command` on `platform` (first = primary). */
|
|
326
|
+
forCommand(command, platform = this.platform) {
|
|
327
|
+
return this.active(platform).filter((e) => e.binding.command === command).map((e) => chordToString(e.chord));
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* The command a keydown event runs, or `null`. Events from text inputs are ignored
|
|
331
|
+
* unless the binding (or `options`) allows them. Bindings in the active scopes
|
|
332
|
+
* (in order) win over global bindings; bindings in other scopes never match.
|
|
333
|
+
* Keys are matched by `event.code`, except that a letter key producing another Latin
|
|
334
|
+
* letter (AZERTY, QWERTZ, Dvorak) matches that letter when Alt is not held, so
|
|
335
|
+
* `Mod+Z` follows the Z label (see `shortcutCode`).
|
|
336
|
+
*/
|
|
337
|
+
resolve(event, options = {}) {
|
|
338
|
+
const platform = options.platform ?? this.platform;
|
|
339
|
+
const editable = isEditableEventTarget(event);
|
|
340
|
+
const scopes = asList(options.scopes);
|
|
341
|
+
const candidates = this.active(platform).filter(
|
|
342
|
+
(e) => !editable || (options.allowInEditable ?? e.binding.allowInEditable ?? false)
|
|
343
|
+
);
|
|
344
|
+
if (candidates.length === 0) return null;
|
|
345
|
+
const code = shortcutCode(event);
|
|
346
|
+
const matching = candidates.filter((e) => this.matches(e.chord, code, event, platform));
|
|
347
|
+
if (matching.length === 0) return null;
|
|
348
|
+
for (const scope of scopes) {
|
|
349
|
+
const found = matching.find((e) => e.binding.scope === scope);
|
|
350
|
+
if (found) return { command: found.binding.command, binding: found.binding };
|
|
351
|
+
}
|
|
352
|
+
const global = matching.find((e) => e.binding.scope === void 0);
|
|
353
|
+
if (global) return { command: global.binding.command, binding: global.binding };
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
matches(chord, code, event, platform) {
|
|
357
|
+
if (chord.key !== code) return false;
|
|
358
|
+
if (!!chord.alt !== !!event.altKey || !!chord.shift !== !!event.shiftKey) return false;
|
|
359
|
+
if (this.options.modifierMode === "either") {
|
|
360
|
+
return !!(chord.mod || chord.ctrl || chord.meta) === !!(event.ctrlKey || event.metaKey);
|
|
361
|
+
}
|
|
362
|
+
const e = effectiveChord(chord, platform);
|
|
363
|
+
return e.ctrl === !!event.ctrlKey && e.meta === !!event.metaKey;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Groups of bindings that share an effective chord in one scope on `platform`
|
|
367
|
+
* (e.g. `Mod+Y` and `Ctrl+Y` collide off macOS). A scoped binding shadowing a
|
|
368
|
+
* global one is not a conflict.
|
|
369
|
+
*/
|
|
370
|
+
findConflicts(platform = this.platform) {
|
|
371
|
+
const groups = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const e of this.active(platform)) {
|
|
373
|
+
const chord = this.options.modifierMode === "either" ? eitherKey(e.chord) : effectiveChordKey(e.chord, platform);
|
|
374
|
+
const id = `${e.binding.scope ?? ""}\0${chord}`;
|
|
375
|
+
const group = groups.get(id) ?? { ...e.binding.scope ? { scope: e.binding.scope } : {}, chord, commands: [] };
|
|
376
|
+
if (!group.commands.includes(e.binding.command)) group.commands.push(e.binding.command);
|
|
377
|
+
groups.set(id, group);
|
|
378
|
+
}
|
|
379
|
+
return [...groups.values()].filter((g) => g.commands.length > 1);
|
|
380
|
+
}
|
|
381
|
+
/** Conflicts on every platform, keyed by platform (only platforms with conflicts). */
|
|
382
|
+
findAllConflicts() {
|
|
383
|
+
const out = {};
|
|
384
|
+
for (const p of PLATFORMS) {
|
|
385
|
+
const conflicts = this.findConflicts(p);
|
|
386
|
+
if (conflicts.length) out[p] = conflicts;
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
function eitherKey(chord) {
|
|
392
|
+
return [(chord.mod || chord.ctrl || chord.meta) && "Mod", chord.alt && "Alt", chord.shift && "Shift", chord.key].filter(Boolean).join("+");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/canvas/arrangeShortcuts.ts
|
|
396
|
+
var DEFAULT_ARRANGE_SHORTCUTS = [
|
|
397
|
+
{ action: "alignLeft", code: "KeyA", alt: true, label: "Alt+A" },
|
|
398
|
+
{ action: "alignCenter", code: "KeyH", alt: true, label: "Alt+H" },
|
|
399
|
+
{ action: "alignRight", code: "KeyD", alt: true, label: "Alt+D" },
|
|
400
|
+
{ action: "alignTop", code: "KeyW", alt: true, label: "Alt+W" },
|
|
401
|
+
{ action: "alignMiddle", code: "KeyV", alt: true, label: "Alt+V" },
|
|
402
|
+
{ action: "alignBottom", code: "KeyS", alt: true, label: "Alt+S" },
|
|
403
|
+
{ action: "distributeHorizontal", code: "KeyH", alt: true, shift: true, label: "Alt+Shift+H" },
|
|
404
|
+
{ action: "distributeVertical", code: "KeyV", alt: true, shift: true, label: "Alt+Shift+V" },
|
|
405
|
+
{ action: "lock", code: "KeyL", mod: true, label: "Mod+L" },
|
|
406
|
+
{ action: "unlock", code: "KeyL", mod: true, alt: true, label: "Mod+Alt+L" },
|
|
407
|
+
{ action: "bringForward", code: "BracketRight", mod: true, label: "Mod+]" },
|
|
408
|
+
{ action: "sendBackward", code: "BracketLeft", mod: true, label: "Mod+[" },
|
|
409
|
+
{ action: "bringToFront", code: "BracketRight", mod: true, alt: true, label: "Mod+Alt+]" },
|
|
410
|
+
{ action: "sendToBack", code: "BracketLeft", mod: true, alt: true, label: "Mod+Alt+[" },
|
|
411
|
+
{ action: "nextScene", code: "PageDown", label: "PageDown" },
|
|
412
|
+
{ action: "previousScene", code: "PageUp", label: "PageUp" }
|
|
413
|
+
];
|
|
414
|
+
var maps = /* @__PURE__ */ new WeakMap();
|
|
415
|
+
function arrangeShortcutMap(bindings) {
|
|
416
|
+
let map = maps.get(bindings);
|
|
417
|
+
if (!map) {
|
|
418
|
+
map = new ShortcutMap(
|
|
419
|
+
bindings.map((b) => ({
|
|
420
|
+
command: b.action,
|
|
421
|
+
chord: { key: b.code, mod: b.mod, alt: b.alt, shift: b.shift }
|
|
422
|
+
})),
|
|
423
|
+
{ modifierMode: "either" }
|
|
424
|
+
);
|
|
425
|
+
maps.set(bindings, map);
|
|
426
|
+
}
|
|
427
|
+
return map;
|
|
428
|
+
}
|
|
429
|
+
function matchArrangeShortcut(event, bindings = DEFAULT_ARRANGE_SHORTCUTS) {
|
|
430
|
+
return arrangeShortcutMap(bindings).resolve(event)?.command ?? null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export {
|
|
434
|
+
PLATFORMS,
|
|
435
|
+
detectPlatform,
|
|
436
|
+
keyToCode,
|
|
437
|
+
codeToKeyText,
|
|
438
|
+
parseChord,
|
|
439
|
+
chordToString,
|
|
440
|
+
normalizeChord,
|
|
441
|
+
effectiveChord,
|
|
442
|
+
effectiveChordKey,
|
|
443
|
+
formatShortcut,
|
|
444
|
+
eventCode,
|
|
445
|
+
shortcutCode,
|
|
446
|
+
codeFromKey,
|
|
447
|
+
isEditableEventTarget,
|
|
448
|
+
toShortcutBindings,
|
|
449
|
+
ShortcutMap,
|
|
450
|
+
DEFAULT_ARRANGE_SHORTCUTS,
|
|
451
|
+
matchArrangeShortcut
|
|
452
|
+
};
|
|
453
|
+
//# sourceMappingURL=chunk-7DWMJCAT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/chords.ts","../src/commands/shortcuts.ts","../src/canvas/arrangeShortcuts.ts"],"sourcesContent":["// Portions ported from filmcraft-main/crates/engine/src/shortcuts.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Keyboard chords in a platform-neutral notation.\n *\n * | token | macOS | Windows / Linux |\n * |---|---|---|\n * | `Mod` | ⌘ Command | Ctrl |\n * | `Ctrl` | ⌃ Control | Ctrl (same key as `Mod`: conflicts are reported) |\n * | `Meta` / `Cmd` | ⌘ Command | Win / Super |\n * | `Alt` / `Option` | ⌥ Option | Alt |\n * | `Shift` | ⇧ Shift | Shift |\n *\n * Keys are stored as `KeyboardEvent.code` values (`KeyZ`, `Digit1`, `BracketRight`,\n * `PageDown`), so Option/Alt chords work on macOS (where Option changes `event.key`).\n */\n\nexport type Platform = 'mac' | 'windows' | 'linux';\n\nexport const PLATFORMS: readonly Platform[] = ['mac', 'windows', 'linux'];\n\n/** A parsed chord. `mod` is the platform's primary modifier. */\nexport interface Chord {\n\t/** `KeyboardEvent.code` of the key, e.g. `KeyZ`. */\n\tkey: string;\n\tmod?: boolean;\n\tctrl?: boolean;\n\tmeta?: boolean;\n\talt?: boolean;\n\tshift?: boolean;\n}\n\n/** The modifiers a chord needs on one platform (`mod` resolved to `meta` or `ctrl`). */\nexport interface EffectiveChord {\n\tkey: string;\n\tctrl: boolean;\n\tmeta: boolean;\n\talt: boolean;\n\tshift: boolean;\n}\n\n/** The current platform from `navigator` (`linux` when unknown, e.g. in Node). */\nexport function detectPlatform(): Platform {\n\tconst nav = typeof navigator !== 'undefined' ? navigator : undefined;\n\tconst text = `${nav?.platform ?? ''} ${nav?.userAgent ?? ''}`;\n\tif (/Mac|iPod|iPhone|iPad/.test(nav?.platform || nav?.userAgent || '')) return 'mac';\n\tif (/Win/.test(text)) return 'windows';\n\treturn 'linux';\n}\n\n// Characters and names → `KeyboardEvent.code`.\nconst NAMED_KEYS: Record<string, string> = {\n\t'[': 'BracketLeft',\n\t']': 'BracketRight',\n\t'-': 'Minus',\n\t'=': 'Equal',\n\tplus: 'Equal',\n\t',': 'Comma',\n\t'.': 'Period',\n\t'/': 'Slash',\n\t'\\\\': 'Backslash',\n\t';': 'Semicolon',\n\t\"'\": 'Quote',\n\t'`': 'Backquote',\n\tenter: 'Enter',\n\treturn: 'Enter',\n\t'↩': 'Enter',\n\tescape: 'Escape',\n\tesc: 'Escape',\n\t'⎋': 'Escape',\n\tspace: 'Space',\n\tspacebar: 'Space',\n\t' ': 'Space',\n\ttab: 'Tab',\n\t'⇥': 'Tab',\n\tbackspace: 'Backspace',\n\t'⌫': 'Backspace',\n\tdelete: 'Delete',\n\tdel: 'Delete',\n\t'⌦': 'Delete',\n\tinsert: 'Insert',\n\thome: 'Home',\n\tend: 'End',\n\tpageup: 'PageUp',\n\tpgup: 'PageUp',\n\tpagedown: 'PageDown',\n\tpgdn: 'PageDown',\n\tup: 'ArrowUp',\n\tarrowup: 'ArrowUp',\n\t'↑': 'ArrowUp',\n\tdown: 'ArrowDown',\n\tarrowdown: 'ArrowDown',\n\t'↓': 'ArrowDown',\n\tleft: 'ArrowLeft',\n\tarrowleft: 'ArrowLeft',\n\t'←': 'ArrowLeft',\n\tright: 'ArrowRight',\n\tarrowright: 'ArrowRight',\n\t'→': 'ArrowRight',\n\tbracketleft: 'BracketLeft',\n\tbracketright: 'BracketRight',\n\tminus: 'Minus',\n\tequal: 'Equal',\n\tcomma: 'Comma',\n\tperiod: 'Period',\n\tslash: 'Slash',\n\tbackslash: 'Backslash',\n\tsemicolon: 'Semicolon',\n\tquote: 'Quote',\n\tbackquote: 'Backquote'\n};\n\nconst CODE_TEXT: Record<string, string> = {\n\tBracketLeft: '[',\n\tBracketRight: ']',\n\tMinus: '-',\n\tEqual: '=',\n\tComma: ',',\n\tPeriod: '.',\n\tSlash: '/',\n\tBackslash: '\\\\',\n\tSemicolon: ';',\n\tQuote: \"'\",\n\tBackquote: '`',\n\tArrowUp: 'Up',\n\tArrowDown: 'Down',\n\tArrowLeft: 'Left',\n\tArrowRight: 'Right'\n};\n\nconst MAC_KEY_GLYPHS: Record<string, string> = {\n\tBackspace: '⌫',\n\tDelete: '⌦',\n\tEnter: '↩',\n\tEscape: '⎋',\n\tTab: '⇥',\n\tArrowUp: '↑',\n\tArrowDown: '↓',\n\tArrowLeft: '←',\n\tArrowRight: '→',\n\tPageUp: 'Page Up',\n\tPageDown: 'Page Down',\n\tSpace: 'Space'\n};\n\nconst KNOWN_CODES = new Set([\n\t...Object.values(NAMED_KEYS),\n\t'Enter',\n\t'Escape',\n\t'Space',\n\t'Tab',\n\t'Backspace',\n\t'Delete',\n\t'Insert',\n\t'Home',\n\t'End',\n\t'PageUp',\n\t'PageDown'\n]);\n\n/** `KeyboardEvent.code` for a key token (`Z`, `1`, `]`, `PageDown`, `F5`, `KeyZ`), or `null`. */\nexport function keyToCode(token: string): string | null {\n\tif (!token) return null;\n\tif (/^[a-z]$/i.test(token)) return `Key${token.toUpperCase()}`;\n\tif (/^[0-9]$/.test(token)) return `Digit${token}`;\n\tif (/^f([1-9]|1[0-9]|2[0-4])$/i.test(token)) return token.toUpperCase();\n\tif (/^Key[A-Z]$/.test(token) || /^Digit[0-9]$/.test(token) || /^Numpad\\w+$/.test(token)) return token;\n\tif (KNOWN_CODES.has(token)) return token;\n\treturn NAMED_KEYS[token] ?? NAMED_KEYS[token.toLowerCase().replace(/[\\s_]/g, '')] ?? null;\n}\n\n/** Neutral text for a code: `KeyZ` → `Z`, `BracketRight` → `]`, `ArrowUp` → `Up`. */\nexport function codeToKeyText(code: string): string {\n\tif (/^Key[A-Z]$/.test(code)) return code.slice(3);\n\tif (/^Digit[0-9]$/.test(code)) return code.slice(5);\n\treturn CODE_TEXT[code] ?? code;\n}\n\nconst GLYPH_MODIFIERS: Record<string, string> = { '⌘': 'Meta+', '⌃': 'Ctrl+', '⌥': 'Alt+', '⇧': 'Shift+' };\n\n/**\n * Parses chord text (`Mod+Shift+Z`, `Alt+]`, `⌥⇧⌘K`, `CmdOrCtrl+K`). Throws on unknown\n * modifiers or keys. The last `+`-separated token is the key (`+` itself is `Plus`).\n */\nexport function parseChord(text: string): Chord {\n\tconst source = text.trim();\n\tif (!source) throw new Error('Empty shortcut.');\n\tlet expanded = '';\n\tfor (const ch of source) expanded += GLYPH_MODIFIERS[ch] ?? ch;\n\tconst parts = expanded\n\t\t.split('+')\n\t\t.map((p) => p.trim())\n\t\t.filter(Boolean);\n\tif (parts.length === 0) throw new Error(`\"${text}\" has no key.`);\n\tconst chord: Chord = { key: '' };\n\tparts.forEach((part, index) => {\n\t\tconst low = part.toLowerCase();\n\t\tconst isLast = index === parts.length - 1;\n\t\tif (low === 'mod' || low === 'cmdorctrl' || low === 'commandorcontrol' || low === 'primary') chord.mod = true;\n\t\telse if (low === 'ctrl' || low === 'control' || low === 'ctl') chord.ctrl = true;\n\t\telse if (low === 'meta' || low === 'cmd' || low === 'command' || low === 'super' || low === 'win') chord.meta = true;\n\t\telse if (low === 'alt' || low === 'option' || low === 'opt') chord.alt = true;\n\t\telse if (low === 'shift') chord.shift = true;\n\t\telse if (isLast) {\n\t\t\tconst code = keyToCode(part);\n\t\t\tif (!code) throw new Error(`Unknown key \"${part}\" in shortcut \"${text}\".`);\n\t\t\tchord.key = code;\n\t\t} else throw new Error(`Unknown modifier \"${part}\" in shortcut \"${text}\".`);\n\t});\n\tif (!chord.key) throw new Error(`\"${text}\" has no key.`);\n\treturn chord;\n}\n\n/** Canonical platform-neutral text: `Mod+Ctrl+Meta+Alt+Shift+<key>`. */\nexport function chordToString(chord: Chord): string {\n\tconst parts: string[] = [];\n\tif (chord.mod) parts.push('Mod');\n\tif (chord.ctrl) parts.push('Ctrl');\n\tif (chord.meta) parts.push('Meta');\n\tif (chord.alt) parts.push('Alt');\n\tif (chord.shift) parts.push('Shift');\n\tparts.push(codeToKeyText(chord.key));\n\treturn parts.join('+');\n}\n\n/** Canonical spelling of chord text (`shift+cmdorctrl+z` → `Mod+Shift+Z`). Throws when invalid. */\nexport function normalizeChord(text: string): string {\n\treturn chordToString(parseChord(text));\n}\n\nconst toChord = (chord: string | Chord): Chord => (typeof chord === 'string' ? parseChord(chord) : chord);\n\n/** The modifiers a chord needs on `platform`: `Mod` is ⌘ (meta) on macOS and Ctrl elsewhere. */\nexport function effectiveChord(chord: string | Chord, platform: Platform): EffectiveChord {\n\tconst c = toChord(chord);\n\tconst mac = platform === 'mac';\n\treturn {\n\t\tkey: c.key,\n\t\tctrl: !!c.ctrl || (!mac && !!c.mod),\n\t\tmeta: !!c.meta || (mac && !!c.mod),\n\t\talt: !!c.alt,\n\t\tshift: !!c.shift\n\t};\n}\n\n/** Identity of an effective chord, for conflict detection (`Ctrl+Shift+KeyZ`). */\nexport function effectiveChordKey(chord: string | Chord, platform: Platform): string {\n\tconst e = effectiveChord(chord, platform);\n\treturn [e.ctrl && 'Ctrl', e.meta && 'Meta', e.alt && 'Alt', e.shift && 'Shift', e.key].filter(Boolean).join('+');\n}\n\n/**\n * Display text: `⌥⇧⌘K` on macOS, `Ctrl+Alt+Shift+K` on Windows / Linux (`Meta` shows as\n * `Win` / `Super`). Invalid text is returned unchanged.\n */\nexport function formatShortcut(chord: string | Chord, platform: Platform = detectPlatform()): string {\n\tlet c: Chord;\n\ttry {\n\t\tc = toChord(chord);\n\t} catch {\n\t\treturn String(chord);\n\t}\n\tconst e = effectiveChord(c, platform);\n\tif (platform === 'mac') {\n\t\tconst mods = `${e.ctrl ? '⌃' : ''}${e.alt ? '⌥' : ''}${e.shift ? '⇧' : ''}${e.meta ? '⌘' : ''}`;\n\t\treturn mods + (MAC_KEY_GLYPHS[c.key] ?? codeToKeyText(c.key));\n\t}\n\tconst parts: string[] = [];\n\tif (e.ctrl) parts.push('Ctrl');\n\tif (e.meta) parts.push(platform === 'windows' ? 'Win' : 'Super');\n\tif (e.alt) parts.push('Alt');\n\tif (e.shift) parts.push('Shift');\n\tconst key = c.key === 'PageUp' ? 'Page Up' : c.key === 'PageDown' ? 'Page Down' : codeToKeyText(c.key);\n\tparts.push(key);\n\treturn parts.join('+');\n}\n\n/** Minimal keyboard event surface (DOM `KeyboardEvent` satisfies it). */\nexport interface KeyboardEventLike {\n\tkey?: string;\n\tcode?: string;\n\tctrlKey?: boolean;\n\tmetaKey?: boolean;\n\taltKey?: boolean;\n\tshiftKey?: boolean;\n\ttarget?: EventTarget | null | unknown;\n\tdefaultPrevented?: boolean;\n\tpreventDefault?: () => void;\n}\n\n/** `event.code`, or one derived from `event.key` for synthetic events without a code. */\nexport function eventCode(event: KeyboardEventLike): string {\n\tif (event.code) return event.code;\n\treturn codeFromKey(event.key ?? '') ?? event.key ?? '';\n}\n\n/**\n * The key code a shortcut event is matched by: `event.code` (layout-independent, so\n * Option/Alt chords work on macOS and punctuation chords follow the physical key),\n * except that a letter key producing a different Latin letter without Alt (AZERTY,\n * QWERTZ, Dvorak) uses the produced letter, as browsers and operating systems do.\n */\nexport function shortcutCode(event: KeyboardEventLike): string {\n\tconst code = eventCode(event);\n\tconst key = event.key ?? '';\n\tif (!event.altKey && /^Key[A-Z]$/.test(code) && /^[a-z]$/i.test(key)) return `Key${key.toUpperCase()}`;\n\treturn code;\n}\n\n/** The code a printable `event.key` names on a US layout (`z` → `KeyZ`, `]` → `BracketRight`). */\nexport function codeFromKey(key: string): string | null {\n\tif (!key) return null;\n\tif (key.length === 1) return /^[a-z0-9]$/i.test(key) || key in NAMED_KEYS ? keyToCode(key) : null;\n\treturn keyToCode(key);\n}\n\n/**\n * Whether a keyboard event comes from a text-entry element: `<input>`, `<textarea>`,\n * `<select>`, a `contenteditable` element (or a descendant) or a ProseMirror editor.\n * Falls back to `document.activeElement` when the event has no element target.\n */\nexport function isEditableEventTarget(event: { target?: unknown }): boolean {\n\ttype ElementLike = {\n\t\ttagName?: unknown;\n\t\tisContentEditable?: boolean;\n\t\tclosest?: (selector: string) => unknown;\n\t\tgetAttribute?: (name: string) => string | null;\n\t};\n\tlet target = (event.target ?? null) as ElementLike | null;\n\t// Window / document targets are not editable; without a target, check the focused element.\n\tif (!target) target = typeof document !== 'undefined' ? (document.activeElement as ElementLike | null) : null;\n\tif (!target || typeof target.tagName !== 'string') return false;\n\tconst tag = target.tagName.toUpperCase();\n\treturn !!(\n\t\ttag === 'INPUT' ||\n\t\ttag === 'TEXTAREA' ||\n\t\ttag === 'SELECT' ||\n\t\ttarget.isContentEditable ||\n\t\ttarget.getAttribute?.('contenteditable') === 'true' ||\n\t\ttarget.closest?.('[contenteditable=\"true\"]') ||\n\t\ttarget.closest?.('.ProseMirror')\n\t);\n}\n","// Portions ported from filmcraft-main/crates/engine/src/shortcuts.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\nimport {\n\tchordToString,\n\tdetectPlatform,\n\teffectiveChord,\n\teffectiveChordKey,\n\tshortcutCode,\n\tisEditableEventTarget,\n\tparseChord,\n\tPLATFORMS,\n\ttype Chord,\n\ttype KeyboardEventLike,\n\ttype Platform\n} from './chords.js';\n\n/** One key binding: a chord that runs `command`, optionally only in one scope / on some platforms. */\nexport interface ShortcutBinding {\n\tcommand: string;\n\t/** Chord text (`Mod+Shift+Z`) or a parsed chord. */\n\tchord: string | Chord;\n\t/** Scope (e.g. `canvas`, `spreadsheet`); scoped bindings win over global ones. Omit for global. */\n\tscope?: string;\n\t/** Platforms the binding exists on (default: all). E.g. `Mod+Y` redo only off macOS. */\n\tplatforms?: readonly Platform[];\n\t/** Also fire while focus is in an input, textarea or contenteditable (default `false`). */\n\tallowInEditable?: boolean;\n}\n\n/** Shortcut declaration on a command: chord text, or an object for platform / scope specifics. */\nexport type ShortcutInput =\n\t| string\n\t| Omit<ShortcutBinding, 'command'>\n\t| ReadonlyArray<string | Omit<ShortcutBinding, 'command'>>;\n\n/** Expands a command's `shortcut` declaration into bindings. */\nexport function toShortcutBindings(command: string, input: ShortcutInput | undefined, scope?: string): ShortcutBinding[] {\n\tif (input === undefined) return [];\n\tconst list = (Array.isArray(input) ? input : [input]) as Array<string | Omit<ShortcutBinding, 'command'>>;\n\treturn list.map((item) =>\n\t\ttypeof item === 'string'\n\t\t\t? { command, chord: item, ...(scope ? { scope } : {}) }\n\t\t\t: { ...item, command, ...(item.scope ?? scope ? { scope: item.scope ?? scope } : {}) }\n\t);\n}\n\nexport interface ShortcutConflict {\n\t/** Scope shared by the bindings (`undefined` = global). */\n\tscope?: string;\n\t/** Effective chord, e.g. `Ctrl+KeyZ`. */\n\tchord: string;\n\t/** Commands bound to it (two or more). */\n\tcommands: string[];\n}\n\nexport interface ShortcutMatch {\n\tcommand: string;\n\tbinding: ShortcutBinding;\n}\n\nexport interface ShortcutResolveOptions {\n\t/** Active scopes, most specific first. Bindings in these scopes win over global ones. */\n\tscopes?: string | readonly string[];\n\t/** Resolve even when the event comes from a text input (default: per binding). */\n\tallowInEditable?: boolean;\n\t/** Override the map's platform. */\n\tplatform?: Platform;\n}\n\nexport interface ShortcutMapOptions {\n\t/** Platform used to resolve `Mod` (default: detected from `navigator` on every resolve). */\n\tplatform?: Platform;\n\t/**\n\t * `platform` (default): `Mod` means ⌘ on macOS and Ctrl elsewhere, exactly.\n\t * `either`: `Mod` (and `Ctrl` / `Meta`) match ⌘ or Ctrl on every platform.\n\t */\n\tmodifierMode?: 'platform' | 'either';\n}\n\ninterface Entry {\n\tbinding: ShortcutBinding;\n\tchord: Chord;\n\tsource: 'default' | 'override';\n}\n\nconst asList = (scopes: string | readonly string[] | undefined): readonly string[] =>\n\tscopes === undefined ? [] : typeof scopes === 'string' ? [scopes] : scopes;\n\n/**\n * Keyboard shortcuts as data: default bindings plus user overrides, resolved from\n * `KeyboardEvent`s by `event.code` (so Option/Alt chords work on macOS), with scopes\n * (scoped bindings win), conflict detection per platform and display formatting.\n */\nexport class ShortcutMap {\n\tprivate defaults: Entry[] = [];\n\tprivate overrides = new Map<string, Entry[]>();\n\tprivate readonly listeners = new Set<() => void>();\n\tprivate readonly options: ShortcutMapOptions;\n\n\tconstructor(bindings: readonly ShortcutBinding[] = [], options: ShortcutMapOptions = {}) {\n\t\tthis.options = options;\n\t\tfor (const binding of bindings) this.defaults.push(this.entry(binding, 'default'));\n\t}\n\n\tget platform(): Platform {\n\t\treturn this.options.platform ?? detectPlatform();\n\t}\n\n\tprivate entry(binding: ShortcutBinding, source: Entry['source']): Entry {\n\t\treturn { binding, chord: typeof binding.chord === 'string' ? parseChord(binding.chord) : binding.chord, source };\n\t}\n\n\tprivate changed() {\n\t\tfor (const listener of Array.from(this.listeners)) listener();\n\t}\n\n\t/** Subscribes to binding changes. Returns an unsubscribe function. */\n\tsubscribe(listener: () => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\t/** Adds default bindings (throws on invalid chord text). Returns a function removing them. */\n\tbind(bindings: ShortcutBinding | readonly ShortcutBinding[]): () => void {\n\t\tconst added = (Array.isArray(bindings) ? bindings : [bindings]).map((b) => this.entry(b as ShortcutBinding, 'default'));\n\t\tif (added.length === 0) return () => {};\n\t\tthis.defaults.push(...added);\n\t\tthis.changed();\n\t\treturn () => {\n\t\t\tconst before = this.defaults.length;\n\t\t\tthis.defaults = this.defaults.filter((e) => !added.includes(e));\n\t\t\tif (this.defaults.length !== before) this.changed();\n\t\t};\n\t}\n\n\t/**\n\t * User override for a command: replaces all of its default bindings with `chords`\n\t * (an empty list unbinds it). Throws on invalid chord text.\n\t */\n\tsetOverride(\n\t\tcommand: string,\n\t\tchords: string | Chord | ReadonlyArray<string | Chord | Omit<ShortcutBinding, 'command'>>\n\t): void {\n\t\tconst list = (Array.isArray(chords) ? chords : [chords]) as Array<string | Chord | Omit<ShortcutBinding, 'command'>>;\n\t\tconst entries = list.map((item) =>\n\t\t\tthis.entry(\n\t\t\t\ttypeof item === 'string' || !('chord' in item) ? { command, chord: item as string | Chord } : { ...item, command },\n\t\t\t\t'override'\n\t\t\t)\n\t\t);\n\t\tthis.overrides.set(command, entries);\n\t\tthis.changed();\n\t}\n\n\t/** Removes a user override (the defaults apply again). */\n\tclearOverride(command: string): void {\n\t\tif (this.overrides.delete(command)) this.changed();\n\t}\n\n\t/** User overrides as plain data (`{ command: ['Mod+K'] }`), e.g. to persist them. */\n\tgetOverrides(): Record<string, string[]> {\n\t\tconst out: Record<string, string[]> = {};\n\t\tfor (const [command, entries] of this.overrides) {\n\t\t\tout[command] = entries.map((e) => chordToString(e.chord));\n\t\t}\n\t\treturn out;\n\t}\n\n\t/** Restores overrides saved with `getOverrides` (replacing the current ones). */\n\tsetOverrides(overrides: Record<string, readonly string[]>): void {\n\t\tthis.overrides.clear();\n\t\tfor (const [command, chords] of Object.entries(overrides)) {\n\t\t\tthis.overrides.set(\n\t\t\t\tcommand,\n\t\t\t\tchords.map((chord) => this.entry({ command, chord }, 'override'))\n\t\t\t);\n\t\t}\n\t\tthis.changed();\n\t}\n\n\tprivate active(platform: Platform): Entry[] {\n\t\tconst out: Entry[] = [];\n\t\tfor (const e of this.defaults) {\n\t\t\tif (!this.overrides.has(e.binding.command)) out.push(e);\n\t\t}\n\t\tfor (const entries of this.overrides.values()) out.push(...entries);\n\t\treturn out.filter((e) => !e.binding.platforms || e.binding.platforms.includes(platform));\n\t}\n\n\t/** Effective bindings on `platform` (defaults without overridden commands, plus overrides). */\n\tbindings(platform: Platform = this.platform): ShortcutBinding[] {\n\t\treturn this.active(platform).map((e) => e.binding);\n\t}\n\n\t/** Canonical chord text bound to `command` on `platform` (first = primary). */\n\tforCommand(command: string, platform: Platform = this.platform): string[] {\n\t\treturn this.active(platform)\n\t\t\t.filter((e) => e.binding.command === command)\n\t\t\t.map((e) => chordToString(e.chord));\n\t}\n\n\t/**\n\t * The command a keydown event runs, or `null`. Events from text inputs are ignored\n\t * unless the binding (or `options`) allows them. Bindings in the active scopes\n\t * (in order) win over global bindings; bindings in other scopes never match.\n\t * Keys are matched by `event.code`, except that a letter key producing another Latin\n\t * letter (AZERTY, QWERTZ, Dvorak) matches that letter when Alt is not held, so\n\t * `Mod+Z` follows the Z label (see `shortcutCode`).\n\t */\n\tresolve(event: KeyboardEventLike, options: ShortcutResolveOptions = {}): ShortcutMatch | null {\n\t\tconst platform = options.platform ?? this.platform;\n\t\tconst editable = isEditableEventTarget(event);\n\t\tconst scopes = asList(options.scopes);\n\t\tconst candidates = this.active(platform).filter(\n\t\t\t(e) => !editable || (options.allowInEditable ?? e.binding.allowInEditable ?? false)\n\t\t);\n\t\tif (candidates.length === 0) return null;\n\t\tconst code = shortcutCode(event);\n\t\tconst matching = candidates.filter((e) => this.matches(e.chord, code, event, platform));\n\t\tif (matching.length === 0) return null;\n\t\tfor (const scope of scopes) {\n\t\t\tconst found = matching.find((e) => e.binding.scope === scope);\n\t\t\tif (found) return { command: found.binding.command, binding: found.binding };\n\t\t}\n\t\tconst global = matching.find((e) => e.binding.scope === undefined);\n\t\tif (global) return { command: global.binding.command, binding: global.binding };\n\t\treturn null;\n\t}\n\n\tprivate matches(chord: Chord, code: string, event: KeyboardEventLike, platform: Platform): boolean {\n\t\tif (chord.key !== code) return false;\n\t\tif (!!chord.alt !== !!event.altKey || !!chord.shift !== !!event.shiftKey) return false;\n\t\tif (this.options.modifierMode === 'either') {\n\t\t\treturn !!(chord.mod || chord.ctrl || chord.meta) === !!(event.ctrlKey || event.metaKey);\n\t\t}\n\t\tconst e = effectiveChord(chord, platform);\n\t\treturn e.ctrl === !!event.ctrlKey && e.meta === !!event.metaKey;\n\t}\n\n\t/**\n\t * Groups of bindings that share an effective chord in one scope on `platform`\n\t * (e.g. `Mod+Y` and `Ctrl+Y` collide off macOS). A scoped binding shadowing a\n\t * global one is not a conflict.\n\t */\n\tfindConflicts(platform: Platform = this.platform): ShortcutConflict[] {\n\t\tconst groups = new Map<string, ShortcutConflict>();\n\t\tfor (const e of this.active(platform)) {\n\t\t\tconst chord = this.options.modifierMode === 'either' ? eitherKey(e.chord) : effectiveChordKey(e.chord, platform);\n\t\t\tconst id = `${e.binding.scope ?? ''}\\u0000${chord}`;\n\t\t\tconst group = groups.get(id) ?? { ...(e.binding.scope ? { scope: e.binding.scope } : {}), chord, commands: [] };\n\t\t\tif (!group.commands.includes(e.binding.command)) group.commands.push(e.binding.command);\n\t\t\tgroups.set(id, group);\n\t\t}\n\t\treturn [...groups.values()].filter((g) => g.commands.length > 1);\n\t}\n\n\t/** Conflicts on every platform, keyed by platform (only platforms with conflicts). */\n\tfindAllConflicts(): Partial<Record<Platform, ShortcutConflict[]>> {\n\t\tconst out: Partial<Record<Platform, ShortcutConflict[]>> = {};\n\t\tfor (const p of PLATFORMS) {\n\t\t\tconst conflicts = this.findConflicts(p);\n\t\t\tif (conflicts.length) out[p] = conflicts;\n\t\t}\n\t\treturn out;\n\t}\n}\n\nfunction eitherKey(chord: Chord): string {\n\treturn [(chord.mod || chord.ctrl || chord.meta) && 'Mod', chord.alt && 'Alt', chord.shift && 'Shift', chord.key]\n\t\t.filter(Boolean)\n\t\t.join('+');\n}\n","import { ShortcutMap } from '../commands/shortcuts.js';\n\n/**\n * Canvas arrangement actions that have keyboard shortcuts.\n */\nexport type CanvasArrangeAction =\n\t| 'alignLeft'\n\t| 'alignCenter'\n\t| 'alignRight'\n\t| 'alignTop'\n\t| 'alignMiddle'\n\t| 'alignBottom'\n\t| 'distributeHorizontal'\n\t| 'distributeVertical'\n\t| 'lock'\n\t| 'unlock'\n\t| 'bringForward'\n\t| 'sendBackward'\n\t| 'bringToFront'\n\t| 'sendToBack'\n\t| 'nextScene'\n\t| 'previousScene';\n\nexport interface ArrangeShortcutBinding {\n\taction: CanvasArrangeAction;\n\t/** `KeyboardEvent.code` (layout- and Option-independent, e.g. `KeyA`, `BracketRight`). */\n\tcode: string;\n\tmod?: boolean;\n\talt?: boolean;\n\tshift?: boolean;\n\t/** Human readable label, e.g. for menus. `Mod` is ⌘ on macOS and Ctrl elsewhere. */\n\tlabel: string;\n}\n\n/**\n * Default bindings. Align/distribute follow Figma (Option/Alt + letter), layer order follows\n * the common `Mod+]` / `Mod+[` convention, lock follows Keynote/Freeform (`Mod+L`,\n * `Mod+Alt+L` to unlock) and scenes step with PageDown / PageUp.\n */\nexport const DEFAULT_ARRANGE_SHORTCUTS: readonly ArrangeShortcutBinding[] = [\n\t{ action: 'alignLeft', code: 'KeyA', alt: true, label: 'Alt+A' },\n\t{ action: 'alignCenter', code: 'KeyH', alt: true, label: 'Alt+H' },\n\t{ action: 'alignRight', code: 'KeyD', alt: true, label: 'Alt+D' },\n\t{ action: 'alignTop', code: 'KeyW', alt: true, label: 'Alt+W' },\n\t{ action: 'alignMiddle', code: 'KeyV', alt: true, label: 'Alt+V' },\n\t{ action: 'alignBottom', code: 'KeyS', alt: true, label: 'Alt+S' },\n\t{ action: 'distributeHorizontal', code: 'KeyH', alt: true, shift: true, label: 'Alt+Shift+H' },\n\t{ action: 'distributeVertical', code: 'KeyV', alt: true, shift: true, label: 'Alt+Shift+V' },\n\t{ action: 'lock', code: 'KeyL', mod: true, label: 'Mod+L' },\n\t{ action: 'unlock', code: 'KeyL', mod: true, alt: true, label: 'Mod+Alt+L' },\n\t{ action: 'bringForward', code: 'BracketRight', mod: true, label: 'Mod+]' },\n\t{ action: 'sendBackward', code: 'BracketLeft', mod: true, label: 'Mod+[' },\n\t{ action: 'bringToFront', code: 'BracketRight', mod: true, alt: true, label: 'Mod+Alt+]' },\n\t{ action: 'sendToBack', code: 'BracketLeft', mod: true, alt: true, label: 'Mod+Alt+[' },\n\t{ action: 'nextScene', code: 'PageDown', label: 'PageDown' },\n\t{ action: 'previousScene', code: 'PageUp', label: 'PageUp' }\n];\n\n// One ShortcutMap per bindings array. `either` keeps the historical behaviour: `Mod` matches ⌘ or Ctrl.\nconst maps = new WeakMap<readonly ArrangeShortcutBinding[], ShortcutMap>();\n\nfunction arrangeShortcutMap(bindings: readonly ArrangeShortcutBinding[]): ShortcutMap {\n\tlet map = maps.get(bindings);\n\tif (!map) {\n\t\tmap = new ShortcutMap(\n\t\t\tbindings.map((b) => ({\n\t\t\t\tcommand: b.action,\n\t\t\t\tchord: { key: b.code, mod: b.mod, alt: b.alt, shift: b.shift }\n\t\t\t})),\n\t\t\t{ modifierMode: 'either' }\n\t\t);\n\t\tmaps.set(bindings, map);\n\t}\n\treturn map;\n}\n\n/**\n * Resolves a keydown event to an arrange action, or `null`. Events from text inputs,\n * textareas, selects, contenteditable and ProseMirror are ignored (see `isEventFromTextInput`).\n * `Mod` is ⌘ or Ctrl. Built on the command system's `ShortcutMap` (matching `event.code`).\n */\nexport function matchArrangeShortcut(\n\tevent: KeyboardEvent,\n\tbindings: readonly ArrangeShortcutBinding[] = DEFAULT_ARRANGE_SHORTCUTS\n): CanvasArrangeAction | null {\n\treturn (arrangeShortcutMap(bindings).resolve(event)?.command as CanvasArrangeAction | undefined) ?? null;\n}\n"],"mappings":"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","names":[]}
|