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
108 changes: 108 additions & 0 deletions apps/dashboard/src/components/command-palette.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { CommentIcon, StarIcon } from "@diffkit/icons";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem as CommandItemUI,
CommandList,
CommandShortcut,
} from "@diffkit/ui/components/command";
import { cn } from "@diffkit/ui/lib/utils";
import { useRouter } from "@tanstack/react-router";
import { formatRelativeTime } from "#/components/pulls/pull-request-row";
import type { CommandItem, CommandItemMeta } from "#/lib/command-palette/types";
import { useCommandItems } from "#/lib/command-palette/use-command-items";
import { useCommandPalette } from "#/lib/command-palette/use-command-palette";

export function CommandPalette() {
const { open, setOpen, close } = useCommandPalette();
const router = useRouter();
const items = useCommandItems();

const groups = new Map<string, CommandItem[]>();
for (const item of items) {
const list = groups.get(item.group) ?? [];
list.push(item);
groups.set(item.group, list);
}

function handleSelect(item: CommandItem) {
close();
if (item.action.type === "navigate") {
void router.navigate({ to: item.action.to });
} else {
void item.action.fn();
}
}

return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{Array.from(groups.entries()).map(([groupName, groupItems]) => (
<CommandGroup key={groupName} heading={groupName}>
{groupItems.map((item) => (
<CommandItemUI
key={item.id}
value={`${item.label} ${(item.keywords ?? []).join(" ")}`}
onSelect={() => handleSelect(item)}
>
{item.icon && (
<item.icon
className={cn("size-4 shrink-0", item.iconClassName)}
/>
)}
<div className="mr-4 min-w-0 flex-1">
<p className="truncate text-sm">{item.label}</p>
{item.meta && <ItemMeta meta={item.meta} />}
</div>
{item.meta?.comments != null && item.meta.comments > 0 && (
<span className="ml-auto flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground">
<CommentIcon className="size-4" />
{item.meta.comments}
</span>
)}
{item.shortcut && <CommandShortcut keys={item.shortcut} />}
</CommandItemUI>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
);
}

function ItemMeta({ meta }: { meta: CommandItemMeta }) {
const parts: string[] = [];
if (meta.repo) parts.push(meta.repo);
if (meta.language) parts.push(meta.language);

if (!parts.length && meta.stars == null && !meta.updatedAt) {
return null;
}

return (
<span className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
{parts.length > 0 && <span>{parts.join(" · ")}</span>}
{meta.stars != null && meta.stars > 0 && (
<>
{parts.length > 0 && <span>·</span>}
<span className="inline-flex items-center gap-0.5">
<StarIcon className="size-4" />
{meta.stars}
</span>
</>
)}
{meta.updatedAt && (
<>
{(parts.length > 0 || (meta.stars != null && meta.stars > 0)) && (
<span>·</span>
)}
<span>{formatRelativeTime(meta.updatedAt)}</span>
</>
)}
</span>
);
}
2 changes: 2 additions & 0 deletions apps/dashboard/src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { getRouteApi, Outlet } from "@tanstack/react-router";
import { CommandPalette } from "#/components/command-palette";
import {
githubMyIssuesQueryOptions,
githubMyPullsQueryOptions,
Expand Down Expand Up @@ -54,6 +55,7 @@ export function DashboardLayout() {
</div>
</div>
</div>
<CommandPalette />
</div>
);
}
74 changes: 74 additions & 0 deletions apps/dashboard/src/lib/command-palette/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
DashboardIcon,
GitPullRequestIcon,
IssuesIcon,
ReviewsIcon,
} from "@diffkit/icons";
import type { CommandItem } from "./types";

let commands: CommandItem[] = [];
const listeners = new Set<() => void>();

function emit() {
for (const listener of listeners) listener();
}

export function registerCommands(items: CommandItem[]): () => void {
commands = [...commands, ...items];
emit();
return () => {
const ids = new Set(items.map((i) => i.id));
commands = commands.filter((c) => !ids.has(c.id));
emit();
};
}

export function getRegisteredCommands(): CommandItem[] {
return commands;
}

export function subscribeCommands(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}

registerCommands([
{
id: "nav:overview",
label: "Go to Overview",
group: "Pages",
icon: DashboardIcon,
keywords: ["home", "dashboard"],
shortcut: ["G", "H"],
action: { type: "navigate", to: "/" },
},
{
id: "nav:pulls",
label: "Go to Pull Requests",
group: "Pages",
icon: GitPullRequestIcon,
keywords: ["pr", "merge"],
shortcut: ["G", "P"],
action: { type: "navigate", to: "/pulls" },
},
{
id: "nav:issues",
label: "Go to Issues",
group: "Pages",
icon: IssuesIcon,
keywords: ["bug", "task"],
shortcut: ["G", "I"],
action: { type: "navigate", to: "/issues" },
},
{
id: "nav:reviews",
label: "Go to Reviews",
group: "Pages",
icon: ReviewsIcon,
keywords: ["review", "requested"],
shortcut: ["G", "R"],
action: { type: "navigate", to: "/reviews" },
},
]);
26 changes: 26 additions & 0 deletions apps/dashboard/src/lib/command-palette/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ComponentType } from "react";

export type CommandAction =
| { type: "navigate"; to: string }
| { type: "execute"; fn: () => void | Promise<void> };

export type CommandItemMeta = {
repo?: string;
comments?: number;
updatedAt?: string;
language?: string | null;
stars?: number;
};

export type CommandItem = {
id: string;
label: string;
group: string;
icon?: ComponentType<{ className?: string }>;
iconClassName?: string;
keywords?: string[];
shortcut?: string[];
action: CommandAction;
priority?: number;
meta?: CommandItemMeta;
};
Loading
Loading