Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { useTheme } from "next-themes";
import { useCallback, useEffect, useRef, useState } from "react";
import { signOutToLogin } from "#/lib/auth-actions";
import { preloadRouteOnce } from "#/lib/route-preload";
import { useGlobalShortcuts } from "#/lib/shortcuts";
import { removeTab, type Tab, useTabs } from "#/lib/tab-store";

interface DashboardTopbarProps {
Expand Down Expand Up @@ -62,6 +63,7 @@ const tabIconMap = {
} as const;

const primaryNavRoutes = ["/", "/pulls", "/issues", "/reviews"] as const;
const MAX_TAB_SHORTCUTS = 9;

export function DashboardTopbar({
user,
Expand Down Expand Up @@ -144,6 +146,50 @@ export function DashboardTopbar({
);
}, [router, tabsReady, openTabs]);

function navigateToTab(tab: Tab | undefined) {
if (!tab) return;
void router.navigate({ to: tab.url });
}

useGlobalShortcuts([
...Array.from(
{ length: Math.min(openTabs.length, MAX_TAB_SHORTCUTS) },
(_, index) => ({
shortcut: { code: `Digit${index + 1}`, shift: true },
enabled: tabsReady,
onTrigger: () => {
navigateToTab(openTabs[index]);
},
}),
),
{
shortcut: { key: "ArrowLeft", shift: true },
enabled: tabsReady && openTabs.length > 1,
onTrigger: () => {
const currentIndex = openTabs.findIndex(
(tab) => tab.url === router.state.location.pathname,
);
const nextIndex =
currentIndex === -1
? openTabs.length - 1
: (currentIndex - 1 + openTabs.length) % openTabs.length;
navigateToTab(openTabs[nextIndex]);
},
},
{
shortcut: { key: "ArrowRight", shift: true },
enabled: tabsReady && openTabs.length > 1,
onTrigger: () => {
const currentIndex = openTabs.findIndex(
(tab) => tab.url === router.state.location.pathname,
);
const nextIndex =
currentIndex === -1 ? 0 : (currentIndex + 1) % openTabs.length;
navigateToTab(openTabs[nextIndex]);
},
},
]);

return (
<nav className="flex min-w-0 items-center gap-3 overflow-hidden px-3 py-2">
<DropdownMenu>
Expand Down
64 changes: 38 additions & 26 deletions apps/dashboard/src/components/pulls/detail/pull-body-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { updatePullBody } from "#/lib/github.functions";
import { type GitHubQueryScope, githubQueryKeys } from "#/lib/github.query";
import type { PullDetail, PullPageData } from "#/lib/github.types";
import { matchesShortcut } from "#/lib/shortcuts";
import { useOptimisticMutation } from "#/lib/use-optimistic-mutation";

export function PullBodySection({
Expand Down Expand Up @@ -68,36 +69,47 @@ export function PullBodySection({

const handleEditorKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
const mod = event.metaKey || event.ctrlKey;
if (!mod) return;
const action = [
{
shortcut: { key: "b", mod: true },
run: () => insertMarkdown("**", "**", "bold"),
},
{
shortcut: { key: "i", mod: true },
run: () => insertMarkdown("_", "_", "italic"),
},
{
shortcut: { key: "e", mod: true },
run: () => insertMarkdown("`", "`", "code"),
},
{
shortcut: { key: "k", mod: true },
run: () => insertMarkdown("[", "](url)", "text"),
},
{
shortcut: { key: "h", mod: true },
run: () => insertMarkdown("### ", "", "heading"),
},
{
shortcut: { key: ".", mod: true, shift: true },
run: () => insertMarkdown("> ", "", "quote"),
},
{
shortcut: { key: "8", mod: true, shift: true },
run: () => insertMarkdown("- ", "", "item"),
},
{
shortcut: { key: "7", mod: true, shift: true },
run: () => insertMarkdown("1. ", "", "item"),
},
].find(({ shortcut }) => matchesShortcut(event, shortcut));

const shortcuts: Record<string, () => void> = {
b: () => insertMarkdown("**", "**", "bold"),
i: () => insertMarkdown("_", "_", "italic"),
e: () => insertMarkdown("`", "`", "code"),
k: () => insertMarkdown("[", "](url)", "text"),
h: () => insertMarkdown("### ", "", "heading"),
};

if (event.shiftKey) {
const shiftShortcuts: Record<string, () => void> = {
".": () => insertMarkdown("> ", "", "quote"),
"8": () => insertMarkdown("- ", "", "item"),
"7": () => insertMarkdown("1. ", "", "item"),
};
const action = shiftShortcuts[event.key];
if (action) {
event.preventDefault();
action();
}
if (!action) {
return;
}

const action = shortcuts[event.key];
if (action) {
event.preventDefault();
action();
}
event.preventDefault();
action.run();
},
[insertMarkdown],
);
Expand Down
93 changes: 26 additions & 67 deletions apps/dashboard/src/lib/command-palette/use-command-palette.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,37 @@
import { useRouter } from "@tanstack/react-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useState } from "react";
import { useGlobalShortcuts } from "#/lib/shortcuts";
import { getRegisteredCommands } from "./registry";

const CHORD_TIMEOUT_MS = 800;

function isEditableTarget(e: KeyboardEvent) {
const tag = (e.target as HTMLElement)?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
if ((e.target as HTMLElement)?.isContentEditable) return true;
return false;
}

export function useCommandPalette() {
const [open, setOpen] = useState(false);
const router = useRouter();
const chordRef = useRef<string[]>([]);
const chordTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);

useEffect(() => {
function resetChord() {
chordRef.current = [];
clearTimeout(chordTimerRef.current);
}

function onKeyDown(e: KeyboardEvent) {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
resetChord();
useGlobalShortcuts([
{
shortcut: { key: "k", mod: true },
allowInEditable: true,
onTrigger: () => {
setOpen((prev) => !prev);
return;
}

if (open || e.metaKey || e.ctrlKey || e.altKey || isEditableTarget(e)) {
return;
}

const key = e.key.toUpperCase();
if (key.length !== 1 || !/[A-Z]/.test(key)) {
resetChord();
return;
}

chordRef.current = [...chordRef.current, key];
clearTimeout(chordTimerRef.current);
chordTimerRef.current = setTimeout(resetChord, CHORD_TIMEOUT_MS);

const pressed = chordRef.current;
const commands = getRegisteredCommands();

for (const cmd of commands) {
if (!cmd.shortcut) continue;
const shortcut = cmd.shortcut.map((k) => k.toUpperCase());

if (shortcut.length !== pressed.length) continue;
if (!shortcut.every((k, i) => k === pressed[i])) continue;

e.preventDefault();
resetChord();

if (cmd.action.type === "navigate") {
void router.navigate({ to: cmd.action.to });
} else {
void cmd.action.fn();
}
return;
}
}

document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
clearTimeout(chordTimerRef.current);
};
}, [open, router]);
},
},
...getRegisteredCommands().flatMap((cmd) => {
if (!cmd.shortcut) return [];
return [
{
shortcut: cmd.shortcut.map((key) => ({ key })),
enabled: !open,
onTrigger: () => {
if (cmd.action.type === "navigate") {
void router.navigate({ to: cmd.action.to });
} else {
void cmd.action.fn();
}
},
},
];
}),
]);

const close = useCallback(() => setOpen(false), []);

Expand Down
Loading
Loading