Skip to content

Commit 563c751

Browse files
authored
Implement keybinds for shell/default pattern (commontoolsinc#1810)
* Jump to note with cmd/ctrl+O [CT-926] * alt+W to return to root * Add `ct-keybind` for alt+N in `default-app.tsx` * Format pass
1 parent bd67563 commit 563c751

File tree

9 files changed

+629
-2
lines changed

9 files changed

+629
-2
lines changed

packages/patterns/default-app.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ export default recipe<CharmsListInput, CharmsListOutput>(
145145
[NAME]: str`DefaultCharmList (${allCharms.length})`,
146146
[UI]: (
147147
<ct-screen>
148+
<ct-keybind
149+
code="KeyN"
150+
alt
151+
preventDefault
152+
onct-keybind={spawnChatbotNote({
153+
allCharms: allCharms as unknown as OpaqueRef<MentionableCharm[]>,
154+
})}
155+
/>
156+
148157
<ct-vstack gap="4" padding="6">
149158
{/* Quick Launch Toolbar */}
150159
<ct-hstack gap="2" align="center">

packages/shell/src/lib/app/commands.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export type Command =
66
| { type: "set-space"; spaceName: string }
77
| { type: "clear-authentication" }
88
| { type: "set-show-charm-list-view"; show: boolean }
9-
| { type: "set-show-debugger-view"; show: boolean };
9+
| { type: "set-show-debugger-view"; show: boolean }
10+
| { type: "set-show-quick-jump-view"; show: boolean };
1011

1112
export function isCommand(value: unknown): value is Command {
1213
if (
@@ -36,6 +37,9 @@ export function isCommand(value: unknown): value is Command {
3637
case "set-show-debugger-view": {
3738
return "show" in value && typeof value.show === "boolean";
3839
}
40+
case "set-show-quick-jump-view": {
41+
return "show" in value && typeof value.show === "boolean";
42+
}
3943
}
4044
return false;
4145
}

packages/shell/src/lib/app/state.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface AppState {
1414
apiUrl: URL;
1515
showShellCharmListView?: boolean;
1616
showDebuggerView?: boolean;
17+
showQuickJumpView?: boolean;
1718
}
1819

1920
export type AppStateSerialized = Omit<AppState, "identity" | "apiUrl"> & {
@@ -55,6 +56,10 @@ export function applyCommand(
5556
next.showDebuggerView = command.show;
5657
break;
5758
}
59+
case "set-show-quick-jump-view": {
60+
next.showQuickJumpView = command.show;
61+
break;
62+
}
5863
}
5964

6065
return next;

packages/shell/src/views/AppView.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Task } from "@lit/task";
1111
import { CharmController } from "@commontools/charm/ops";
1212
import { CellEventTarget, CellUpdateEvent } from "../lib/cell-event-target.ts";
1313
import { NAME } from "@commontools/runner";
14-
import { updatePageTitle } from "../lib/navigate.ts";
14+
import { navigate, updatePageTitle } from "../lib/navigate.ts";
1515

1616
export class XAppView extends BaseView {
1717
static override styles = css`
@@ -56,15 +56,78 @@ export class XAppView extends BaseView {
5656
private titleSubscription?: CellEventTarget<string | undefined>;
5757

5858
private debuggerController = new DebuggerController(this);
59+
private _onGlobalKeyDown = (e: KeyboardEvent) => {
60+
// Ignore when focusing editable elements
61+
const target = e.target as HTMLElement | null;
62+
const tag = (target?.tagName || "").toLowerCase();
63+
const isEditable = !!(
64+
target &&
65+
(target.isContentEditable ||
66+
tag === "input" ||
67+
tag === "textarea" ||
68+
tag === "select")
69+
);
70+
if (isEditable) return;
71+
72+
// Track modifier state explicitly; some layouts emit separate events
73+
this._updateModifierState(e, true);
74+
75+
const isMac = navigator.platform.toLowerCase().includes("mac");
76+
const hasMod = isMac ? this._metaDown : this._ctrlDown;
77+
const onlyMod = hasMod && !this._shiftDown && !this._altDown;
78+
const altOnly = this._altDown && !this._shiftDown && !this._metaDown &&
79+
!this._ctrlDown;
80+
if (onlyMod && e.code === "KeyO") {
81+
e.preventDefault();
82+
this.command({ type: "set-show-quick-jump-view", show: true });
83+
return;
84+
}
85+
86+
if (altOnly && e.code === "KeyW") {
87+
e.preventDefault();
88+
const spaceName = this.app?.spaceName ?? "common-knowledge";
89+
navigate({ type: "space", spaceName });
90+
}
91+
};
92+
93+
private _onGlobalKeyUp = (e: KeyboardEvent) => {
94+
this._updateModifierState(e, false);
95+
};
96+
97+
private _altDown = false;
98+
private _metaDown = false;
99+
private _ctrlDown = false;
100+
private _shiftDown = false;
101+
102+
private _updateModifierState(e: KeyboardEvent, down: boolean) {
103+
switch (e.key) {
104+
case "Alt":
105+
this._altDown = down;
106+
break;
107+
case "Meta":
108+
this._metaDown = down;
109+
break;
110+
case "Control":
111+
this._ctrlDown = down;
112+
break;
113+
case "Shift":
114+
this._shiftDown = down;
115+
break;
116+
}
117+
}
59118

60119
override connectedCallback() {
61120
super.connectedCallback();
62121
// Listen for clear telemetry events
63122
this.addEventListener("clear-telemetry", this.handleClearTelemetry);
123+
document.addEventListener("keydown", this._onGlobalKeyDown);
124+
document.addEventListener("keyup", this._onGlobalKeyUp);
64125
}
65126

66127
override disconnectedCallback() {
67128
this.removeEventListener("clear-telemetry", this.handleClearTelemetry);
129+
document.removeEventListener("keydown", this._onGlobalKeyDown);
130+
document.removeEventListener("keyup", this._onGlobalKeyUp);
68131
super.disconnectedCallback();
69132
}
70133

@@ -184,6 +247,10 @@ export class XAppView extends BaseView {
184247
.visible="${this.debuggerController.isVisible()}"
185248
.telemetryMarkers="${this.debuggerController.getTelemetryMarkers()}"
186249
></x-debugger-view>
250+
<x-quick-jump-view
251+
.visible="${this.app?.showQuickJumpView ?? false}"
252+
.rt="${this.rt}"
253+
></x-quick-jump-view>
187254
`
188255
: ""}
189256
`;

0 commit comments

Comments
 (0)