diff --git a/.github/workflows/shortcuts-plugin.yml b/.github/workflows/shortcuts-plugin.yml new file mode 100644 index 00000000..a6f2c5e5 --- /dev/null +++ b/.github/workflows/shortcuts-plugin.yml @@ -0,0 +1,20 @@ +name: Shortcuts plugin check +on: + pull_request: + merge_group: + +permissions: + contents: read + pull-requests: write + issues: write + actions: read + +jobs: + package-check: + uses: ./.github/workflows/package-check.yml + with: + package-name: '@editorjs/shortcuts-plugin' + working-directory: './packages/plugins/shortcuts-plugin' + include-mutations: true + secrets: + stryker_dashboard_api_key: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.gitignore b/.gitignore index fbb2f010..226b830c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ jest-report.json **/.env /--help/ .claude/settings.local.json +.claude/worktrees/ diff --git a/openspec/specs/core/spec.md b/openspec/specs/core/spec.md index 05a06a9c..c37d790d 100644 --- a/openspec/specs/core/spec.md +++ b/openspec/specs/core/spec.md @@ -95,13 +95,3 @@ The system SHALL provide `UndoRedoManager`, which batches consecutive model even - **THEN** the default undo/redo behavior is suppressed Implemented in `src/components/UndoRedoManager.ts`, validated by its co-located `.spec.ts`. - -### Requirement: Keyboard shortcuts plugin -The system SHALL provide a `ShortcutsPlugin` (an `EditorjsPlugin`) that maps keyboard shortcuts declared in a tool's `options.shortcut` to inline-tool application through the `EditorAPI`. - -#### Scenario: Triggering an inline tool via shortcut -- **GIVEN** an inline tool is registered with `options.shortcut` set to a key combination (e.g. `CMD+B`) -- **WHEN** that key combination is pressed while the editor has focus -- **THEN** `ShortcutsPlugin` applies the corresponding inline tool to the current selection via the `EditorAPI` - -Implemented in `src/plugins/ShortcutsPlugin.ts`. diff --git a/openspec/specs/shortcuts-plugin/spec.md b/openspec/specs/shortcuts-plugin/spec.md new file mode 100644 index 00000000..a0d2573a --- /dev/null +++ b/openspec/specs/shortcuts-plugin/spec.md @@ -0,0 +1,49 @@ +# Shortcuts Plugin + +## Purpose + +`@editorjs/shortcuts-plugin` is a built-in `EditorjsPlugin` that maps keyboard shortcuts declared in a tool's `options.shortcut` to inline-tool application through the `EditorAPI`. It subscribes to tool-loaded events to collect shortcuts and to delegated keydown events to dispatch them. + +## Requirements + +### Requirement: Keyboard shortcuts plugin +The system SHALL provide a `ShortcutsPlugin` (an `EditorjsPlugin`) that, on construction, subscribes to the `core:tool:loaded` and `ui:key-down` events, registers each loaded tool's string `options.shortcut`, and applies the matching inline tool to the current selection via the `EditorAPI` when its shortcut is pressed. + +#### Scenario: Registering a shortcut from a loaded tool +- **GIVEN** a tool is loaded whose merged `options.shortcut` is a string (e.g. `CMD+B`) +- **WHEN** the `core:tool:loaded` event fires +- **THEN** the plugin registers a mapping from that shortcut string to the tool's name + +#### Scenario: Ignoring tools without a string shortcut +- **GIVEN** a tool is loaded whose `options.shortcut` is absent or is not a string +- **WHEN** the `core:tool:loaded` event fires +- **THEN** the plugin registers no shortcut for that tool + +#### Scenario: Triggering an inline tool via shortcut +- **GIVEN** an inline tool is registered with `options.shortcut` set to a key combination (e.g. `CMD+B`) +- **WHEN** that key combination is pressed while the editor has focus +- **THEN** the plugin prevents the native event's default action and applies the corresponding inline tool to the current selection via `api.selection.applyInlineTool` + +#### Scenario: Matching only the first registered shortcut +- **GIVEN** more than one registered shortcut would match the keydown +- **WHEN** the `ui:key-down` event fires +- **THEN** the plugin applies only the first matching tool and stops + +#### Scenario: Ignoring keydown during IME composition +- **GIVEN** the native keydown event has `isComposing === true` +- **WHEN** the `ui:key-down` event fires +- **THEN** the plugin performs no matching and leaves the native event untouched + +#### Scenario: Tolerating a missing caret when applying a tool +- **GIVEN** applying the inline tool throws an `IndexError` (e.g. no caret in a text input) +- **WHEN** a matching shortcut is dispatched +- **THEN** the plugin swallows the error and leaves the editor unchanged, while any other error propagates + +#### Scenario: Releasing shortcuts on destroy +- **GIVEN** a `ShortcutsPlugin` instance has registered shortcuts +- **WHEN** `destroy()` is called +- **THEN** it clears the registered shortcuts so subsequent keydowns dispatch nothing + +Shortcuts for block tools and block tunes (a `shortcuts` map in tool `options`) are reserved for future work and not yet implemented. + +Implemented in `src/index.ts`, validated by its co-located `.spec.ts`. diff --git a/packages/core/package.json b/packages/core/package.json index 532ca108..28367e5b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -49,6 +49,7 @@ "@editorjs/model": "workspace:^", "@editorjs/paragraph": "workspace:^", "@editorjs/sdk": "workspace:^", + "@editorjs/shortcuts-plugin": "workspace:^", "inversify": "^8.1.0", "reflect-metadata": "^0.2.2" } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4f9d0c7d..72388a27 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,7 +23,7 @@ import { Paragraph } from '@editorjs/paragraph'; import { BoldInlineTool } from '@editorjs/bold'; import { ItalicInlineTool } from '@editorjs/italic'; import { LinkInlineTool } from '@editorjs/inline-link'; -import { ShortcutsPlugin } from './plugins/ShortcutsPlugin.js'; +import { ShortcutsPlugin } from '@editorjs/shortcuts-plugin'; import { DOMAdapters } from '@editorjs/dom-adapters'; import { BlocksManager } from './components/BlockManager.js'; import { BlockRenderer } from './components/BlockRenderer.js'; diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json index 617135c5..fef2dbc8 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -30,6 +30,9 @@ }, { "path": "../plugins/clipboard-plugin/tsconfig.build.json" + }, + { + "path": "../plugins/shortcuts-plugin/tsconfig.build.json" } ] } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index da578afd..670b7f59 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -49,6 +49,9 @@ }, { "path": "../plugins/clipboard-plugin/tsconfig.build.json" + }, + { + "path": "../plugins/shortcuts-plugin/tsconfig.build.json" } ] } diff --git a/packages/plugins/shortcuts-plugin/.gitignore b/packages/plugins/shortcuts-plugin/.gitignore new file mode 100644 index 00000000..854a697d --- /dev/null +++ b/packages/plugins/shortcuts-plugin/.gitignore @@ -0,0 +1,24 @@ +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Swap the comments on the following lines if you don't wish to use zero-installs +# Documentation here: https://yarnpkg.com/features/zero-installs +#!.yarn/cache +#.pnp.* + +# IDE +.idea/* + +node_modules/* +dist/* + +# tests +coverage/ +reports/ + +# stryker temp files +.stryker-tmp diff --git a/packages/plugins/shortcuts-plugin/README.md b/packages/plugins/shortcuts-plugin/README.md new file mode 100644 index 00000000..99f65523 --- /dev/null +++ b/packages/plugins/shortcuts-plugin/README.md @@ -0,0 +1,3 @@ +# Shortcuts Plugin + +Built-in Editor.js Plugin that maps keyboard shortcuts declared in a tool's `options.shortcut` to inline-tool application. diff --git a/packages/plugins/shortcuts-plugin/eslint.config.mjs b/packages/plugins/shortcuts-plugin/eslint.config.mjs new file mode 100644 index 00000000..e822a5e9 --- /dev/null +++ b/packages/plugins/shortcuts-plugin/eslint.config.mjs @@ -0,0 +1,39 @@ +import CodeX from 'eslint-config-codex'; + +export default [ + ...CodeX, + { + languageOptions: { + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: import.meta.dirname, + sourceType: 'module', + }, + }, + rules: { + 'n/no-unpublished-import': ['error', { + allowModules: [ + 'eslint-config-codex', + ], + ignoreTypeImport: true, + }], + 'n/no-missing-import': 'off', + '@typescript-eslint/unbound-method': 'off', + 'n/no-unsupported-features/node-builtins': ['error', { + version: '>=24.0.0', + ignores: [], + }], + }, + }, + { + files: ['**/*.spec.ts'], + rules: { + /** + * For test files allow dev dependencies imports + */ + 'n/no-unpublished-import': ['error', { + allowModules: ['@jest/globals'], + }], + }, + }, +]; diff --git a/packages/plugins/shortcuts-plugin/jest.config.ts b/packages/plugins/shortcuts-plugin/jest.config.ts new file mode 100644 index 00000000..8133bf5f --- /dev/null +++ b/packages/plugins/shortcuts-plugin/jest.config.ts @@ -0,0 +1,31 @@ +import type { JestConfigWithTsJest } from 'ts-jest'; + +export default { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: [ '/src/**/*.spec.ts' ], + extensionsToTreatAsEsm: [ '.ts' ], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + coverageReporters: ['lcov', 'json-summary', 'text-summary'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + '^.+\\.jsx?$': [ + 'babel-jest', + { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + ], + }, + ], + }, + transformIgnorePatterns: [ + 'node_modules/(?!@editorjs)', + ], +} as JestConfigWithTsJest; diff --git a/packages/plugins/shortcuts-plugin/package.json b/packages/plugins/shortcuts-plugin/package.json new file mode 100644 index 00000000..37c290a1 --- /dev/null +++ b/packages/plugins/shortcuts-plugin/package.json @@ -0,0 +1,42 @@ +{ + "name": "@editorjs/shortcuts-plugin", + "version": "0.0.0", + "packageManager": "yarn@4.0.1", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "yarn clear && tsc --build tsconfig.build.json", + "build:declaration": "yarn build --emitDeclarationOnly", + "lint": "eslint ./src", + "lint:ci": "yarn lint --max-warnings 0", + "lint:fix": "yarn lint --fix", + "test": "node --experimental-vm-modules $(yarn bin jest)", + "test:coverage": "yarn test --coverage=true", + "test:mutations": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" stryker run", + "clear": "rm -rf dist && rm -f tsconfig.build.tsbuildinfo && rm -f tsconfig.tsbuildinfo" + }, + "devDependencies": { + "@babel/core": "^7.29.0", + "@babel/preset-env": "^7.29.2", + "@jest/globals": "^29.7.0", + "@stryker-mutator/core": "^7.0.2", + "@stryker-mutator/jest-runner": "^7.0.2", + "@stryker-mutator/typescript-checker": "^7.0.2", + "@types/eslint": "^9.6.1", + "@types/jest": "^29.5.12", + "@types/node": "^22.10.2", + "babel-jest": "^29.7.0", + "eslint": "^9.24.0", + "eslint-config-codex": "^2.0.3", + "eslint-plugin-import": "^2.31.0", + "jest": "^29.7.0", + "stryker-cli": "^1.0.2", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.5.4" + }, + "dependencies": { + "@editorjs/sdk": "workspace:^" + } +} diff --git a/packages/plugins/shortcuts-plugin/src/index.spec.ts b/packages/plugins/shortcuts-plugin/src/index.spec.ts new file mode 100644 index 00000000..e8599158 --- /dev/null +++ b/packages/plugins/shortcuts-plugin/src/index.spec.ts @@ -0,0 +1,210 @@ +/* eslint-disable jsdoc/require-jsdoc, @typescript-eslint/naming-convention */ +import { jest } from '@jest/globals'; +import type { CoreConfigValidated, EditorAPI, EditorjsPluginParams } from '@editorjs/sdk'; + +type Listener = (e: Event) => void; + +/** + * `matchKeyboardShortcut` is mocked so tests drive matching explicitly rather than + * simulating real key codes. Its behavior is validated in `@editorjs/sdk`. + */ +const matchKeyboardShortcut = jest.fn<(event: KeyboardEvent, shortcut: string) => boolean>(); + +class IndexError extends Error {} + +jest.unstable_mockModule('@editorjs/sdk', () => { + return { + CoreEventType: { ToolLoaded: 'tool:loaded' }, + KeydownUIEventName: 'key-down', + PluginType: { Plugin: 'Plugin' }, + IndexError, + matchKeyboardShortcut, + EventBus: jest.fn().mockImplementation(() => { + const listeners = new Map(); + + return { + addEventListener: jest.fn((event: string, fn: Listener) => listeners.set(event, fn)), + removeEventListener: jest.fn((event: string) => listeners.delete(event)), + __fire: (event: string, evt: Event) => listeners.get(event)?.(evt), + }; + }), + }; +}); + +const { EventBus, CoreEventType, KeydownUIEventName } = await import('@editorjs/sdk'); +const { ShortcutsPlugin } = await import('./index.js'); + +type FireableEventBus = InstanceType & { __fire: (event: string, evt: Event) => void }; + +const TOOL_LOADED_EVENT = `core:${CoreEventType.ToolLoaded}`; +const KEYDOWN_EVENT = `ui:${KeydownUIEventName}`; + +describe('ShortcutsPlugin', () => { + let pluginParamsMock: EditorjsPluginParams; + let applyInlineTool: jest.Mock; + + beforeEach(() => { + matchKeyboardShortcut.mockReset(); + + applyInlineTool = jest.fn(); + pluginParamsMock = { + eventBus: new EventBus(), + api: { selection: { applyInlineTool } } as unknown as EditorAPI, + config: {} as CoreConfigValidated, + }; + }); + + function bus(): FireableEventBus { + return pluginParamsMock.eventBus as unknown as FireableEventBus; + } + + /** + * Emits a `core:tool:loaded` event carrying a tool facade with the given name and options. + * @param name - tool name reported by the facade + * @param options - merged tool options (e.g. `{ shortcut: 'CMD+B' }`) + */ + function loadTool(name: string, options: Record): void { + bus().__fire(TOOL_LOADED_EVENT, { detail: { tool: { name, + options } } } as unknown as Event); + } + + /** + * Emits a `ui:key-down` event and returns the native event's `preventDefault` spy. + * @param nativeEvent - partial native keydown event overrides + */ + function pressKey(nativeEvent: Partial = {}): jest.Mock { + const preventDefault = jest.fn(); + + bus().__fire(KEYDOWN_EVENT, { detail: { nativeEvent: { preventDefault, + isComposing: false, + ...nativeEvent } } } as unknown as Event); + + return preventDefault; + } + + describe('constructor()', () => { + it('should add tool-loaded listener', () => { + new ShortcutsPlugin(pluginParamsMock); + + expect(pluginParamsMock.eventBus.addEventListener).toHaveBeenCalledWith(TOOL_LOADED_EVENT, expect.any(Function)); + }); + + it('should add keydown listener', () => { + new ShortcutsPlugin(pluginParamsMock); + + expect(pluginParamsMock.eventBus.addEventListener).toHaveBeenCalledWith(KEYDOWN_EVENT, expect.any(Function)); + }); + }); + + describe('tool-loaded listener', () => { + it('should register a shortcut when the tool options provide a string shortcut', () => { + new ShortcutsPlugin(pluginParamsMock); + loadTool('bold', { shortcut: 'CMD+B' }); + matchKeyboardShortcut.mockReturnValue(true); + + pressKey(); + + expect(matchKeyboardShortcut).toHaveBeenCalledWith(expect.anything(), 'CMD+B'); + expect(applyInlineTool).toHaveBeenCalledWith({ tool: 'bold' }); + }); + + it('should ignore a tool whose shortcut option is not a string', () => { + new ShortcutsPlugin(pluginParamsMock); + loadTool('bold', { shortcut: 123 }); + matchKeyboardShortcut.mockReturnValue(true); + + pressKey(); + + expect(matchKeyboardShortcut).not.toHaveBeenCalled(); + expect(applyInlineTool).not.toHaveBeenCalled(); + }); + + it('should ignore a tool with no shortcut option', () => { + new ShortcutsPlugin(pluginParamsMock); + loadTool('paragraph', {}); + matchKeyboardShortcut.mockReturnValue(true); + + pressKey(); + + expect(matchKeyboardShortcut).not.toHaveBeenCalled(); + expect(applyInlineTool).not.toHaveBeenCalled(); + }); + }); + + describe('keydown listener', () => { + beforeEach(() => { + new ShortcutsPlugin(pluginParamsMock); + loadTool('bold', { shortcut: 'CMD+B' }); + }); + + it('should apply the inline tool and prevent default when a shortcut matches', () => { + matchKeyboardShortcut.mockReturnValue(true); + + const preventDefault = pressKey(); + + expect(preventDefault).toHaveBeenCalledTimes(1); + expect(applyInlineTool).toHaveBeenCalledWith({ tool: 'bold' }); + }); + + it('should do nothing when no shortcut matches', () => { + matchKeyboardShortcut.mockReturnValue(false); + + const preventDefault = pressKey(); + + expect(preventDefault).not.toHaveBeenCalled(); + expect(applyInlineTool).not.toHaveBeenCalled(); + }); + + it('should ignore the event while IME composition is in progress', () => { + matchKeyboardShortcut.mockReturnValue(true); + + const preventDefault = pressKey({ isComposing: true }); + + expect(matchKeyboardShortcut).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); + expect(applyInlineTool).not.toHaveBeenCalled(); + }); + + it('should apply only the first matching shortcut', () => { + loadTool('italic', { shortcut: 'CMD+I' }); + matchKeyboardShortcut.mockReturnValue(true); + + pressKey(); + + expect(applyInlineTool).toHaveBeenCalledTimes(1); + expect(applyInlineTool).toHaveBeenCalledWith({ tool: 'bold' }); + }); + + it('should swallow an IndexError thrown while applying the inline tool', () => { + matchKeyboardShortcut.mockReturnValue(true); + applyInlineTool.mockImplementation(() => { + throw new IndexError('no caret'); + }); + + expect(() => pressKey()).not.toThrow(); + }); + + it('should rethrow a non-IndexError thrown while applying the inline tool', () => { + matchKeyboardShortcut.mockReturnValue(true); + applyInlineTool.mockImplementation(() => { + throw new Error('boom'); + }); + + expect(() => pressKey()).toThrow('boom'); + }); + }); + + describe('.destroy()', () => { + it('should clear registered shortcuts so later keydowns dispatch nothing', () => { + const plugin = new ShortcutsPlugin(pluginParamsMock); + + loadTool('bold', { shortcut: 'CMD+B' }); + plugin.destroy(); + matchKeyboardShortcut.mockReturnValue(true); + + pressKey(); + + expect(applyInlineTool).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/src/plugins/ShortcutsPlugin.ts b/packages/plugins/shortcuts-plugin/src/index.ts similarity index 100% rename from packages/core/src/plugins/ShortcutsPlugin.ts rename to packages/plugins/shortcuts-plugin/src/index.ts diff --git a/packages/plugins/shortcuts-plugin/stryker.conf.mjs b/packages/plugins/shortcuts-plugin/stryker.conf.mjs new file mode 100644 index 00000000..9b1f4ca1 --- /dev/null +++ b/packages/plugins/shortcuts-plugin/stryker.conf.mjs @@ -0,0 +1,35 @@ +// @ts-check +/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ +const config = { + _comment: + "This config was generated using 'stryker init'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information.", + packageManager: "yarn", + thresholds: { + break: 75, + }, + thresholds_comment: "Minimum required coverage. Increase once we're closer to 100%.", + clearTextReporter: { + allowEmojis: true, + }, + reporters: [ + "json", + "html", + "clear-text", + "progress", + "dashboard", + ], + testRunner: "jest", + testRunner_comment: + "Take a look at https://stryker-mutator.io/docs/stryker-js/jest-runner for information about the jest plugin.", + coverageAnalysis: "perTest", + tsconfigFile: "tsconfig.json", + checkers: ["typescript"], + timeoutMS: 10000, + mutate: ["./src/**/*.ts", "!./src/**/mocks/*.ts", "!./src/**/__mocks__/*.ts", "!./src/**/*.spec.ts"], + /* + * In some cases PRs might not have any unit-tests + */ + allowEmpty: true, +}; + +export default config; diff --git a/packages/plugins/shortcuts-plugin/tsconfig.build.json b/packages/plugins/shortcuts-plugin/tsconfig.build.json new file mode 100644 index 00000000..5b7ae0fc --- /dev/null +++ b/packages/plugins/shortcuts-plugin/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules/**/*", + "dist/**/*", + "**/*.spec.ts" + ], + "references": [ + { + "path": "../../sdk/tsconfig.build.json" + } + ] +} diff --git a/packages/plugins/shortcuts-plugin/tsconfig.eslint.json b/packages/plugins/shortcuts-plugin/tsconfig.eslint.json new file mode 100644 index 00000000..72ee87bc --- /dev/null +++ b/packages/plugins/shortcuts-plugin/tsconfig.eslint.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "types": ["node", "jest"] + }, + "extends": "./tsconfig.json", + "include": [ + "src/**/*", + "eslint.config.mjs", + "**/*.spec.ts", + "jest.config.ts", + "stryker.conf.mjs" + ], + "exclude": [ + "dist/**/*", + "node_modules/**/*" + ] +} diff --git a/packages/plugins/shortcuts-plugin/tsconfig.json b/packages/plugins/shortcuts-plugin/tsconfig.json new file mode 100644 index 00000000..8cc9fa49 --- /dev/null +++ b/packages/plugins/shortcuts-plugin/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "composite": true, + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": [ + "node_modules/**/*", + "dist/**/*" + ], + "references": [ + { + "path": "../../sdk/tsconfig.build.json" + } + ] +} diff --git a/yarn.lock b/yarn.lock index 127a480f..ff882eb1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2339,6 +2339,7 @@ __metadata: "@editorjs/model": "workspace:^" "@editorjs/paragraph": "workspace:^" "@editorjs/sdk": "workspace:^" + "@editorjs/shortcuts-plugin": "workspace:^" "@jest/globals": "npm:^29.7.0" "@stryker-mutator/core": "npm:^7.0.2" "@stryker-mutator/jest-runner": "npm:^7.0.2" @@ -2598,6 +2599,32 @@ __metadata: languageName: unknown linkType: soft +"@editorjs/shortcuts-plugin@workspace:^, @editorjs/shortcuts-plugin@workspace:packages/plugins/shortcuts-plugin": + version: 0.0.0-use.local + resolution: "@editorjs/shortcuts-plugin@workspace:packages/plugins/shortcuts-plugin" + dependencies: + "@babel/core": "npm:^7.29.0" + "@babel/preset-env": "npm:^7.29.2" + "@editorjs/sdk": "workspace:^" + "@jest/globals": "npm:^29.7.0" + "@stryker-mutator/core": "npm:^7.0.2" + "@stryker-mutator/jest-runner": "npm:^7.0.2" + "@stryker-mutator/typescript-checker": "npm:^7.0.2" + "@types/eslint": "npm:^9.6.1" + "@types/jest": "npm:^29.5.12" + "@types/node": "npm:^22.10.2" + babel-jest: "npm:^29.7.0" + eslint: "npm:^9.24.0" + eslint-config-codex: "npm:^2.0.3" + eslint-plugin-import: "npm:^2.31.0" + jest: "npm:^29.7.0" + stryker-cli: "npm:^1.0.2" + ts-jest: "npm:^29.2.5" + ts-node: "npm:^10.9.2" + typescript: "npm:^5.5.4" + languageName: unknown + linkType: soft + "@editorjs/ui-kit@npm:^1.1.5": version: 1.1.5 resolution: "@editorjs/ui-kit@npm:1.1.5"