ショートカットとコマンドパレット
文脈依存のコマンドレジストリ、検索可能なショートカットオーバーレイ(?)、そしてファジー検索できるコマンドパレット(⌘⇧K)。
commands.ts は、フラットで 文脈依存のコマンドレジストリ です。キーボードショートカットから到達できるアプリレベルのアクション——ツール切り替え、undo/redo、クリップボード、削除、整列、ファイル操作、ヘルプ——は、すべて 1 つの CommandDef エントリです。2 つの UI サーフェスがこのリストをそのまま消費します。Editor.tsx のキーボード駆動フォールバックディスパッチャと、このページが扱う 2 つのダイアログ——検索可能な ショートカットオーバーレイ(?)とファジー検索できる コマンドパレット(⌘/Ctrl+Shift+K)です。あるコマンドのチョード、ラベル、有効性が「keydown で何が発火するか」と「ショートカット UI が何を一覧表示するか」の間でずれることは決してありません。両方とも、まったく同じ配列を読んでいるからです。
Note
これは、古い手作業で維持していた静的なショートカットテーブル(dialogs/)を置き換えるものです——そのファイルはもう存在しません。ダイアログ id とファイル名は今は shortcut-panel です。
コマンドレジストリ
interface CommandDef {
id: string;
label: string;
category: string; // Tool | Edit | View | Align | File | Text | Help
chord?: Chord; // 無ければキーボードから到達不可(パレット専用)
displayOnly?: boolean; // 実際のジェスチャーは他のコードが所有(Paste、Nudge)
preventDefault?: boolean;
run(ctx: CommandContext): void;
isEnabled(ctx: CommandContext): boolean;
shortcutDisplay?: string; // チョードから導出される表示文字列を上書きする
}すべてのコマンドは、呼び出しのたびに ctx を その都度読み直します ——コマンドは登録時にあらかじめバインドされたクロージャでは決してありません。これは、リファレンス実装の CommandCallbacks 間接層からの意図的な逸脱です。ツール切り替えのコマンドも手書きの一覧ではありません。toolCommands() は呼び出しのたびにツールレジストリから 1 コマンドずつを導出するため、ツール自身の shortcut フィールドが、ツールバーとこのリストの両方にとって、唯一の情報源であり続けます。
以下の 2 つのフラグは、あるコマンドが下記の 2 つの UI サーフェスにどう参加するかを、それぞれ独立に変えます。
chordが無い("chordless") — そのコマンドにはキーボードショートカットが一切なく、コマンドパレットからしか到達できません。Align、Distribute、New Panel、Import/Download JSON、Download Gerber (.zip)、3 つのズームアクション、Browse Google Fonts はすべて、設計上 chordless です。displayOnly: true— そのコマンドは chord(あるいは明示的なshortcutDisplay)を持ち、表示する価値はありますが、実際のジェスチャーは別のコードが所有していて、レジストリ経由でrun()が呼ばれることは決してありません。Paste(実際のジェスチャーはネイティブのpasteイベント——クリップボード → ペーストを参照)と Nudge(Arrows、Shift で ×10——Editor.tsx自身の矢印キーのスイッチ)の 2 つだけです。
チョードのマッチング
interface Chord {
key: string | readonly string[]; // 1 つのキー、または同じショートカットを意味する複数のキー
meta?: boolean; // undefined = 気にしない。true/false = 押されている/いないことが必須
shift?: boolean;
alt?: boolean;
}meta は metaKey または ctrlKey にマッチします。どちらのプラットフォームでも同じです——チョードはこれまでもプラットフォームでゲートされたことはありません(⌘/Ctrl+Z は Mac でも Windows/Linux でも undo します)。is-mac.ts(navigator.platform.includes('Mac')。iPad/iPhone とタッチ対応の MacIntel は除外)が決めるのは どのグリフを表示するか ——⌘/⌥/⇧ か Ctrl+/Alt+/Shift+ か——だけです。どの物理キーを受理するかを決めることは決してありません。
ディスパッチの順序
Editor.tsx のグローバルな keydown ハンドラは、あらゆるキーを、アプリの他の部分とまったく同じ順序でルーティングします(ツール → ポインタ/キーボードイベントの契約を参照)。
アクティブなツール の
onKeyDownが最初の拒否権を持ちます。ツールがそのキーを取らなければ、
dispatchCommand()が、チョードがマッチする、有効かつ非displayOnlyの最初のコマンドを見つけて実行します(コマンドが要求していれば、先にpreventDefault()します)。レジストリの何もマッチしなければ、小さな専用のスイッチが 矢印キーによるナッジ を処理します——4 つのキーとシフトによるステップ倍率は、1 つの
Chordにはまとまらないため、ナッジはレジストリの外に留まっています(edit-nudgeは一覧に載せられるようdisplayOnlyエントリとしてだけ存在します)。
<input>/<textarea>/<select> にフォーカスがある間、キーボード入力はグローバルに無視されます。レイヤー名変更フィールドや数値フィールドへの入力が、ショートカットを発火させることはありません。
ショートカットオーバーレイ
修飾キーなしの ? キー(help-shortcuts コマンドのチョードは単に { key: '?' })、またはヘッダーの ? ボタンで開きます。dialogs/ は、表示できるもの——本物の chord、または明示的な shortcutDisplay——を持つすべてのコマンドを一覧表示します。これには、実行可能なコマンド(ツール切り替え、undo/redo、クリップボード、削除、選択解除、ヘルプ/パレット自体)と、2 つの displayOnly エントリ(Paste、Nudge)の両方が含まれ、category でグループ化され、検索ボックスで絞り込めます。
検索 — テキスト入力(プレースホルダーは "Search shortcuts…")が、コマンドの
labelまたはcategoryに対する部分一致でフィルタします(例えば "edit" と入力すると Edit グループ全体が現れます)。一致がなければ "No shortcuts match "‹query›"" と表示されます。グルーピング — 一致した項目は、レジストリの順序のまま、大文字のカテゴリ見出しの下にグループ化されます。
フォーカス — 検索入力は開いたときに自動でフォーカスされます(レイアウトエフェクトによる工夫で、ダイアログホストの汎用的な「最初のフォーカス可能な要素にフォーカスする」フォールバックとの競争に意図的に勝ちます。DOM 順で Close ボタンの方が先にあるためです——これがなければ、フィルタ用のキー入力のつもりが、モーダルの裏でエディタのショートカットを再び発火させてしまいかねません)。
Chordless なコマンド(Align、New Panel、Import/Download JSON、Download Gerber (.zip)、zoom、Browse Google Fonts)は、ここには 現れません ——表示するものが何もないためです。それらを実行したいときはコマンドパレットを使ってください。
コマンドパレット
⌘/Ctrl+Shift+K で開きます。dialogs/ は、実行可能な すべてのコマンド(displayOnly のエントリは除外されます——Paste や Nudge を一覧から実行しても、黙って何も起きないだけだからです)を対象にした、ファジー検索可能でキーボード操作できるリストです。
ファジー検索 — 手書きのサブシーケンスマッチャー(
fuzzyScore)です。クエリのすべての文字が、順序どおりに(必ずしも連続している必要はなく)対象の中に現れなければなりません("cpy"は"Copy"にマッチします)。コマンドのlabelとcategoryを結合したものに対してスコアリングするため、"align"のようなクエリは、どのラベルにもその部分文字列が literal には含まれていなくても、Align のコマンドをすべて表示します。スコアが低いほどタイトなマッチです(連続した一致はコストゼロ。マッチした文字の間のギャップはその長さぶんコストが加算されます)。結果はマッチが良い順にソートされます。クエリが空のときは最近使ったもの — 最近実行したコマンド id が最大 8 件、
zpd.palette-recents.v1というlocalStorageキーの下に、新しい順・重複なしで永続化されます。入力を始めるまでは、これらがリストの先頭に来ます(それ以外は自然なレジストリの順序)。入力を始めると、完全にファジーランキングに切り替わります。キーボード操作 —
↑/↓でハイライトを移動(リストの範囲でクランプされます)、Enterでハイライト中のコマンドを実行、Escapeはここでは意図的に処理 しません ——共有のダイアログホストの Escape-to-close リスナー(後述)へバブルします。無効な行は表示されたままだが操作できない —
isEnabled(ctx)が現在 false のコマンド(例えば Browse Google Fonts は、textレイヤーが選択されているときのみ有効)は、リストから消えるのではなく、薄く表示されたままクリックできない状態になります。コマンドを実行すると、それが「最近」として記録され、パレットは閉じます——ただし、そのコマンド自身が 別の ダイアログを開いた場合(例えば Keyboard Shortcuts は
shortcut-panelを、Browse Google Fonts はfont-explorerを開きます)は例外です。ダイアログストアは同時に 1 つのダイアログしか追跡しないため、パレットは、run()がちょうど何かを開いたあとも自分がまだ開いているダイアログであるかを確認してから閉じます。
共有のダイアログクロム
この 2 つのダイアログはどちらも(登録されているすべてのダイアログと同様——追加アクションとダイアログを参照)、components/ を通じてレンダリングされます。これは次のすべてを一手に担う唯一の存在です。role="dialog" aria-modal="true" のバックドロップ、開いている間ダイアログ内に閉じ込められるフォーカストラップ、最初のフォーカス可能な要素への初期フォーカス(ショートカットオーバーレイの検索ボックスのように、すでに自分でフォーカスを動かしたコンポーネントには譲ります)、Escape での閉じる操作、そしてダイアログが開く前にフォーカスされていた要素へのフォーカスの復元です。登録されるダイアログのコンポーネントはコンテンツだけを提供し、このクロムをダイアログごとに再実装することはありません。