Skip to content
Merged
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
185 changes: 122 additions & 63 deletions resources/js/components/keyboard-shortcuts-dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Fragment } from 'react';
import {
Dialog,
DialogContent,
Expand All @@ -7,59 +8,117 @@ import {
import type { KeyLabels } from '@/lib/keyboard';
import { useKeyLabels } from '@/lib/keyboard';

type Shortcut = { keys: string[]; label: string };
// Each entry in `keys` is one way to press it: most have one, search has two.
type Shortcut = { keys: string[][]; label: string };
type Group = { title: string; shortcuts: Shortcut[] };

const shortcuts = (keys: KeyLabels, hasRemotes: boolean): Shortcut[] => [
{ keys: ['j'], label: 'Next commit' },
{ keys: ['k'], label: 'Previous commit' },
{ keys: ['y'], label: 'Copy link to the focused commit' },
{ keys: ['↵'], label: 'Open the focused commit' },
{ keys: ['r'], label: 'Reply to commit' },
{ keys: ['q'], label: 'Quote commit' },
{ keys: ['s'], label: 'Star or unstar commit' },
{ keys: ['o'], label: 'Show or hide replies' },
{ keys: ['c'], label: 'Write a commit' },
{ keys: [keys.mod, 'K'], label: 'Search developers and commits' },
{ keys: ['/'], label: 'Search developers and commits' },
{ keys: [keys.mod, '↵'], label: 'Commit (in the composer)' },
{ keys: [keys.mod, keys.shift, 'P'], label: 'Preview (in the composer)' },
{ keys: ['g', 'l'], label: 'Go to your log' },
{ keys: ['g', 'e'], label: 'Go to trending' },
{ keys: ['g', 'p'], label: 'Go to your profile' },
{ keys: ['g', 's'], label: 'Go to settings' },
{ keys: ['g', 'n'], label: 'Go to notifications' },
...(hasRemotes ? [{ keys: ['g', 'm'], label: 'Go to remotes' }] : []),
{ keys: ['g', '*'], label: 'Go to starred' },
{ keys: ['?'], label: 'Show this list' },
const groups = (
keys: KeyLabels,
hasRemotes: boolean,
canModerate: boolean,
): Group[][] => [
[
{
title: 'Commits',
shortcuts: [
{ keys: [['j']], label: 'Next' },
{ keys: [['k']], label: 'Previous' },
{ keys: [['↵']], label: 'Open' },
{ keys: [['r']], label: 'Reply' },
{ keys: [['q']], label: 'Quote' },
{ keys: [['s']], label: 'Star or unstar' },
{ keys: [['o']], label: 'Show or hide replies' },
{ keys: [['y']], label: 'Copy link' },
],
},
],
[
{
title: 'Go to',
shortcuts: [
{ keys: [['g', 'l']], label: 'Your log' },
{ keys: [['g', 'e']], label: 'Trending' },
{ keys: [['g', 'p']], label: 'Your profile' },
{ keys: [['g', 'n']], label: 'Notifications' },
...(hasRemotes
? [{ keys: [['g', 'm']], label: 'Remotes' }]
: []),
{ keys: [['g', '*']], label: 'Starred' },
{ keys: [['g', 's']], label: 'Settings' },
],
},
],
[
{
title: 'Write',
shortcuts: [
{ keys: [['c']], label: 'New commit' },
{ keys: [[keys.mod, '↵']], label: 'Commit' },
{ keys: [[keys.mod, keys.shift, 'P']], label: 'Preview' },
],
},
{
title: 'Anywhere',
shortcuts: [
{ keys: [[keys.mod, 'K'], ['/']], label: 'Search' },
{ keys: [['?']], label: 'Show this list' },
],
},
...(canModerate
? [
{
title: 'Moderation queue',
shortcuts: [
{ keys: [['a']], label: 'Approve commit' },
{ keys: [['x']], label: 'Reject commit' },
],
},
]
: []),
],
];

const moderationShortcuts: Shortcut[] = [
{ keys: ['a'], label: 'Approve commit (in the queue)' },
{ keys: ['x'], label: 'Reject commit (in the queue)' },
];
function ShortcutGroup({ group }: { group: Group }) {
const id = `shortcuts-${group.title.toLowerCase().replaceAll(' ', '-')}`;

function ShortcutList({ shortcuts }: { shortcuts: Shortcut[] }) {
return (
<dl className="divide-y text-sm">
{shortcuts.map((shortcut) => (
<div
key={shortcut.keys.join('')}
className="flex items-center justify-between gap-4 py-2"
>
<dt>{shortcut.label}</dt>
<dd className="flex gap-1">
{shortcut.keys.map((key) => (
<kbd
key={key}
className="min-w-6 rounded border border-b-2 bg-muted px-1.5 py-0.5 text-center font-mono text-xs"
>
{key}
</kbd>
))}
</dd>
</div>
))}
</dl>
<section aria-labelledby={id}>
<h3
id={id}
className="mb-1 text-xs font-medium tracking-wide text-muted-foreground uppercase"
>
{group.title}
</h3>
<dl className="divide-y text-sm">
{group.shortcuts.map((shortcut) => (
<div
key={shortcut.label}
className="flex items-center justify-between gap-4 py-2"
>
<dt>{shortcut.label}</dt>
<dd className="flex items-center gap-1">
{shortcut.keys.map((combo, index) => (
<Fragment key={combo.join('')}>
{index > 0 && (
<span className="px-0.5 text-xs text-muted-foreground">
or
</span>
)}
{combo.map((key) => (
<kbd
key={key}
className="min-w-6 rounded border border-b-2 bg-muted px-1.5 py-0.5 text-center font-mono text-xs"
>
{key}
</kbd>
))}
</Fragment>
))}
</dd>
</div>
))}
</dl>
</section>
);
}

Expand All @@ -78,25 +137,25 @@ export function KeyboardShortcutsDialog({

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-sm">
<DialogContent className="overflow-y-auto sm:max-w-sm md:max-w-3xl">
<DialogTitle>Keyboard shortcuts</DialogTitle>
<DialogDescription>
They work anywhere except while you're typing.
They work anywhere except while you're typing. Commit and
preview work in the composer.
</DialogDescription>

<ShortcutList shortcuts={shortcuts(keys, hasRemotes)} />

{canModerate && (
<section aria-labelledby="moderation-shortcuts">
<h3
id="moderation-shortcuts"
className="mb-1 text-sm font-semibold"
>
Moderation
</h3>
<ShortcutList shortcuts={moderationShortcuts} />
</section>
)}
<div className="grid gap-x-8 gap-y-6 md:grid-cols-3">
{groups(keys, hasRemotes, canModerate).map((column) => (
<div key={column[0].title} className="space-y-6">
{column.map((group) => (
<ShortcutGroup
key={group.title}
group={group}
/>
))}
</div>
))}
</div>
</DialogContent>
</Dialog>
);
Expand Down