Squashed commit of the following:

commit b2b0470a1d9267d042ec0ac66523c6635bf5b199
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:13:38 2026 +0200

    chore: update .gitignore to include .vite-hooks and modify pnpm-lock.yaml for dependencies

commit d28fadb5cd8706c874e616102878b4a394ec84c1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 13:08:04 2026 +0200

    fix: remove timestamp conflict guard

commit c6998d9dbab19d09d3c8054feef1d2e4117555eb
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 12:11:51 2026 +0200

    chore(release): v5.1.5

commit f33d168711804880e1f12e88d24290aae16cc258
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:35 2026 +0200

    revert: compose.yml

commit d961e6535811a10c335525fb33a08d03e737278d
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:58:08 2026 +0200

    refactor(agent): replace 'revert' terminology with 'restore' for clarity, resolves #3086

commit 17f351171be218e33f01c469d95e4164d4c8dc57
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 11:10:41 2026 +0200

    refactor(pdf): simplify sidebar section filtering and update summary feature logic

commit d55179b9d76879e3204de185e8b53fadd0a107ed
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:53:37 2026 +0200

    chore: update pnpm-lock.yaml and turbo.json

commit 7cade6980e1a04352536bd44ef773f338c4ef599
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:38:30 2026 +0200

    fix(polyfill): add tested polyfill for Map Upsert methods

commit 26d175bb9c53d93225d1e907678445252c13d660
Merge: 1cf33dc6c 5b1297fa2
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:23:29 2026 +0200

    Merge remote-tracking branch 'origin/main' into feat/explore-hono-orpc-migration

    # Conflicts:
    #	packages/api/src/services/agent-url.ts
    #	packages/runtime-externals/package.json

commit 1cf33dc6c9d81735730ad656e16dab6501c6d6a1
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Tue May 19 09:22:12 2026 +0200

    chore: preserve branch changes before main sync

commit b380a4b00fdbcdd81ff4f8ef72b330fd027ccda5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Mon May 18 07:50:28 2026 +0200

    chore: lot of fixes for monorepo migration

commit 8fcf0ec64e1c29572ebaff494338368bfcf75760
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 13:57:17 2026 +0200

    chore: update knip version and refine web app routing with new SEO endpoints

commit 234e68086ff15610a93877354c98e2c020364533
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 12:10:06 2026 +0200

    refactor(auth): update OAuth routes to include API prefix and remove unused schema endpoint

commit 91c84b9a8496b0ce21d71cae9f8b2a027638c9ac
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:54:29 2026 +0200

    chore: update dependencies and enhance PWA metadata in web app

commit 150117d4a5a9dd6cd92c64891aad8cae90f6a7af
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:12:35 2026 +0200

    docs: revise manifest-only pwa testing scope

commit 6b939a55661aec9dd8122b184e4b60a5c7325fb5
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:11:33 2026 +0200

    docs: add manifest-only pwa design

commit 1422e1fc96c400948b273210a1067251087d15d4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:05:04 2026 +0200

    chore(dev): simplify server proxy config

commit bc2ff5a9f6fda41e6c40333c8f163aa23a6c5e48
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:50 2026 +0200

    docs: add unsafe oauth redirect plan

commit 445359ebe9b96c1515bf1c4c3f73ba8a8448ec12
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 11:04:34 2026 +0200

    feat(auth): add unsafe oauth redirect flag

commit 73fffdd24598e56b2793f7657919bc794835892e
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:55:02 2026 +0200

    docs: design unsafe oauth redirect flag

commit c0066aa19c15fc8a4c8e5179ed49889c117519f4
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:22:04 2026 +0200

    chore: update translation source paths

commit 9033da082418d252aafd6c2eed72f71f014be3d9
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 10:09:25 2026 +0200

    refactor(arch): react spa + hono migration

commit 6f27936c11bda895977dc63ee550c3346d4ce24b
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Fri May 15 01:10:47 2026 +0200

    docs: add docker nightly tagging design

commit ecc1fd9a88a0ee1dca2f1977dfc17f74527fe1da
Author: Amruth Pillai <im.amruth@gmail.com>
Date:   Thu May 14 20:05:44 2026 +0200

    feat: migrate to hono spa server
This commit is contained in:
Amruth Pillai
2026-05-19 13:14:21 +02:00
parent 5b1297fa2b
commit 62f8270b3e
518 changed files with 29398 additions and 26871 deletions
@@ -0,0 +1,143 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useHotkeys } from "@tanstack/react-hotkeys";
import { useRef } from "react";
import { Command, CommandEmpty, CommandInput, CommandList } from "@reactive-resume/ui/components/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@reactive-resume/ui/components/dialog";
import { NavigationCommandGroup } from "./pages/navigation";
import { PreferencesCommandGroup } from "./pages/preferences";
import { ResumesCommandGroup } from "./pages/resumes";
import { useCommandPaletteStore } from "./store";
export function CommandPalette() {
const inputRef = useRef<HTMLInputElement>(null);
const { open, search, pages, setOpen, setSearch, goBack } = useCommandPaletteStore();
const isFirstPage = pages.length === 0;
const currentPage = pages[pages.length - 1];
// Toggle command palette with Cmd+K / Ctrl+K
useHotkeys([
{
hotkey: "Mod+K",
callback: () => {
setOpen(!open);
},
},
{
hotkey: "Escape",
callback: () => {
if (!open) return;
setOpen(false);
},
},
{
hotkey: "Backspace",
callback: (event) => {
// Only handle if the command palette is open
if (!open) return;
const input = inputRef.current;
if (!input) return;
// Only handle if input is focused
if (document.activeElement !== input) return;
// If input has text, let the default behavior handle it (delete character)
if (search.length > 0) return;
// If input is empty, prevent default and go back
event.preventDefault();
goBack();
},
},
]);
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
};
const handleSearchChange = (value: string) => {
setSearch(value);
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogHeader className="sr-only print:hidden">
<DialogTitle>
<Trans comment="Screen-reader dialog title for the command palette in the resume builder">
Builder Command Palette
</Trans>
</DialogTitle>
<DialogDescription>
<Trans comment="Screen-reader dialog description instructing users how to use the command palette">
Type a command or search...
</Trans>
</DialogDescription>
</DialogHeader>
<DialogContent
className="overflow-hidden p-0"
aria-label={
isFirstPage
? t({
comment: "Accessible label for the command palette dialog",
message: "Command Palette",
})
: t({
comment: "Accessible label for command palette dialog when browsing a nested command page",
message: `Command Palette - ${currentPage}`,
})
}
>
<Command
loop
aria-label={t({
comment: "Accessible label for command list region inside command palette",
message: "Command Palette",
})}
className="[&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground **:[[cmdk-group]]:px-2 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3"
>
<CommandInput
ref={inputRef}
value={search}
onValueChange={handleSearchChange}
placeholder={
isFirstPage
? t({
comment: "Placeholder in command palette input on root page",
message: "Type a command or search...",
})
: t({
comment: "Placeholder in command palette input on nested pages",
message: "Search...",
})
}
aria-label={t({
comment: "Accessible label for command palette search input",
message: "Search commands",
})}
/>
<CommandList>
<CommandEmpty>
<Trans comment="Empty-state message when no command palette results match the search query">
The command you're looking for doesn't exist.
</Trans>
</CommandEmpty>
<ResumesCommandGroup />
<PreferencesCommandGroup />
<NavigationCommandGroup />
</CommandList>
</Command>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,53 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { Command } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../store";
import { BaseCommandGroup } from "./base";
const renderInCommand = (ui: React.ReactNode) => render(<Command>{ui}</Command>);
const resetStore = () => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
};
afterEach(resetStore);
describe("BaseCommandGroup", () => {
it("renders children at the root (no page prop) when the page stack is empty", () => {
renderInCommand(<BaseCommandGroup heading="Root">child-text</BaseCommandGroup>);
expect(screen.getByText("child-text")).toBeInTheDocument();
});
it("does NOT render when the page stack tops a different page than its `page` prop", () => {
useCommandPaletteStore.setState({ pages: ["other"] });
const { container } = renderInCommand(
<BaseCommandGroup page="settings" heading="Settings">
<span>x</span>
</BaseCommandGroup>,
);
// Nothing rendered besides the Command shell itself.
expect(container.textContent).toBe("");
});
it("renders when the top of the page stack matches its `page` prop", () => {
useCommandPaletteStore.setState({ pages: ["settings"] });
renderInCommand(
<BaseCommandGroup page="settings" heading="Settings">
settings-children
</BaseCommandGroup>,
);
expect(screen.getByText("settings-children")).toBeInTheDocument();
});
it("does NOT render the root group when there is a sub-page on top", () => {
useCommandPaletteStore.setState({ pages: ["settings"] });
const { container } = renderInCommand(
<BaseCommandGroup heading="Root">
<span>root-text</span>
</BaseCommandGroup>,
);
expect(container.textContent).toBe("");
});
});
@@ -0,0 +1,22 @@
import { useMemo } from "react";
import { CommandGroup } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../store";
type Props = {
page?: string;
heading: React.ReactNode;
children: React.ReactNode;
};
export const BaseCommandGroup = ({ page, heading, children }: Props) => {
const pages = useCommandPaletteStore((state) => state.pages);
const currentPage = pages[pages.length - 1];
const isEnabled = useMemo(() => {
return currentPage === page;
}, [currentPage, page]);
if (!isEnabled) return null;
return <CommandGroup heading={heading}>{children}</CommandGroup>;
};
@@ -0,0 +1,126 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import {
ChatCircleDotsIcon,
GearIcon,
HouseSimpleIcon,
KeyIcon,
OpenAiLogoIcon,
ReadCvLogoIcon,
ShieldCheckIcon,
UserCircleIcon,
WarningIcon,
} from "@phosphor-icons/react";
import { useNavigate, useRouteContext } from "@tanstack/react-router";
import { CommandItem } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../store";
import { BaseCommandGroup } from "./base";
export function NavigationCommandGroup() {
const navigate = useNavigate();
const { session } = useRouteContext({ strict: false });
const reset = useCommandPaletteStore((state) => state.reset);
const pushPage = useCommandPaletteStore((state) => state.pushPage);
const onNavigate = async (path: string) => {
await navigate({ to: path });
reset();
};
return (
<>
<BaseCommandGroup heading={<Trans>Go to...</Trans>}>
<CommandItem keywords={[t`Home`]} value="navigation.home" onSelect={() => onNavigate("/")}>
<HouseSimpleIcon />
<Trans>Home</Trans>
</CommandItem>
<CommandItem
disabled={!session}
keywords={[t`Resumes`]}
value="navigation.resumes"
onSelect={() => onNavigate("/dashboard/resumes")}
>
<ReadCvLogoIcon />
<Trans>Resumes</Trans>
</CommandItem>
<CommandItem
disabled={!session}
keywords={[t`Agent`, t`Artificial Intelligence`]}
value="navigation.agent"
onSelect={() => onNavigate("/agent")}
>
<ChatCircleDotsIcon />
<Trans>Agent</Trans>
</CommandItem>
<CommandItem
disabled={!session}
keywords={[t`Settings`]}
value="navigation.settings"
onSelect={() => pushPage("settings")}
>
<GearIcon />
<Trans>Settings</Trans>
</CommandItem>
</BaseCommandGroup>
<BaseCommandGroup page="settings" heading={<Trans>Settings</Trans>}>
<CommandItem
keywords={[t`Profile`]}
value="navigation.settings.profile"
onSelect={() => onNavigate("/dashboard/settings/profile")}
>
<UserCircleIcon />
<Trans>Profile</Trans>
</CommandItem>
<CommandItem
keywords={[t`Preferences`]}
value="navigation.settings.preferences"
onSelect={() => onNavigate("/dashboard/settings/preferences")}
>
<GearIcon />
<Trans>Preferences</Trans>
</CommandItem>
<CommandItem
keywords={[t`Authentication`]}
value="navigation.settings.authentication"
onSelect={() => onNavigate("/dashboard/settings/authentication")}
>
<ShieldCheckIcon />
<Trans>Authentication</Trans>
</CommandItem>
<CommandItem
keywords={[t`API Keys`]}
value="navigation.settings.api-keys"
onSelect={() => onNavigate("/dashboard/settings/api-keys")}
>
<KeyIcon />
<Trans>API Keys</Trans>
</CommandItem>
<CommandItem
keywords={[t`Integrations`, t`Artificial Intelligence`]}
value="navigation.settings.integrations"
onSelect={() => onNavigate("/dashboard/settings/integrations")}
>
<OpenAiLogoIcon />
<Trans>Integrations</Trans>
</CommandItem>
<CommandItem
keywords={[t`Danger Zone`]}
value="navigation.settings.danger-zone"
onSelect={() => onNavigate("/dashboard/settings/danger-zone")}
>
<WarningIcon />
<Trans>Danger Zone</Trans>
</CommandItem>
</BaseCommandGroup>
</>
);
}
@@ -0,0 +1,52 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Command } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../../store";
vi.mock("@/features/theme/provider", () => ({
useTheme: () => ({ setTheme: vi.fn(), theme: "light", toggleTheme: vi.fn() }),
}));
const { PreferencesCommandGroup } = await import("./index");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
});
const renderGroup = () =>
render(
<I18nProvider i18n={i18n}>
<Command>
<PreferencesCommandGroup />
</Command>
</I18nProvider>,
);
describe("PreferencesCommandGroup", () => {
it("renders 'Change theme to...' and 'Change language to...' at the root", () => {
renderGroup();
expect(screen.getByText("Change theme to...")).toBeInTheDocument();
expect(screen.getByText("Change language to...")).toBeInTheDocument();
});
it("pushes 'theme' onto the page stack when the theme item is selected", () => {
renderGroup();
const item = screen.getByText("Change theme to...");
fireEvent.click(item);
expect(useCommandPaletteStore.getState().pages).toContain("theme");
});
it("pushes 'language' onto the page stack when the language item is selected", () => {
renderGroup();
fireEvent.click(screen.getByText("Change language to..."));
expect(useCommandPaletteStore.getState().pages).toContain("language");
});
});
@@ -0,0 +1,30 @@
import { Trans } from "@lingui/react/macro";
import { PaletteIcon, TranslateIcon } from "@phosphor-icons/react";
import { CommandItem } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../../store";
import { BaseCommandGroup } from "../base";
import { LanguageCommandPage } from "./language";
import { ThemeCommandPage } from "./theme";
export function PreferencesCommandGroup() {
const pushPage = useCommandPaletteStore((state) => state.pushPage);
return (
<>
<BaseCommandGroup heading={<Trans>Preferences</Trans>}>
<CommandItem onSelect={() => pushPage("theme")}>
<PaletteIcon />
<Trans>Change theme to...</Trans>
</CommandItem>
<CommandItem onSelect={() => pushPage("language")}>
<TranslateIcon />
<Trans>Change language to...</Trans>
</CommandItem>
</BaseCommandGroup>
<ThemeCommandPage />
<LanguageCommandPage />
</>
);
}
@@ -0,0 +1,59 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Command } from "@reactive-resume/ui/components/command";
import { localeMap } from "@/libs/locale";
import { useCommandPaletteStore } from "../../store";
import { LanguageCommandPage } from "./language";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
});
const renderPage = () =>
render(
<I18nProvider i18n={i18n}>
<Command>
<LanguageCommandPage />
</Command>
</I18nProvider>,
);
describe("LanguageCommandPage", () => {
it("does NOT render when the page stack does not have 'language' on top", () => {
renderPage();
// localeMap codes shouldn't appear because BaseCommandGroup gating is off.
expect(screen.queryByText("en-US")).toBeNull();
});
it("renders one CommandItem for every entry in localeMap when active", () => {
useCommandPaletteStore.setState({ pages: ["language"] });
renderPage();
const expectedCount = Object.keys(localeMap).length;
expect(expectedCount).toBeGreaterThan(0);
// Each locale value is rendered in the inline font-mono span.
for (const code of Object.keys(localeMap).slice(0, 5)) {
expect(screen.getByText(code)).toBeInTheDocument();
}
});
it("includes the documented set of locales (sample check)", () => {
useCommandPaletteStore.setState({ pages: ["language"] });
renderPage();
// Spot-check a couple of common locales.
for (const code of ["en-US", "de-DE", "ja-JP"]) {
if (code in localeMap) {
expect(screen.getByText(code)).toBeInTheDocument();
}
}
});
});
@@ -0,0 +1,27 @@
import { useLingui } from "@lingui/react";
import { Trans } from "@lingui/react/macro";
import { CommandItem } from "@reactive-resume/ui/components/command";
import { isLocale, loadLocale, localeMap, setLocaleCookie } from "@/libs/locale";
import { BaseCommandGroup } from "../base";
export function LanguageCommandPage() {
const { i18n } = useLingui();
const handleLocaleChange = async (value: string) => {
if (!value || !isLocale(value)) return;
setLocaleCookie(value);
await loadLocale(value);
window.location.reload();
};
return (
<BaseCommandGroup page="language" heading={<Trans>Language</Trans>}>
{Object.entries(localeMap).map(([value, label]) => (
<CommandItem key={value} onSelect={() => handleLocaleChange(value)}>
<span className="font-mono text-muted-foreground text-xs">{value}</span>
{i18n.t(label)}
</CommandItem>
))}
</BaseCommandGroup>
);
}
@@ -0,0 +1,45 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Command } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../../store";
vi.mock("@/features/theme/provider", () => ({
useTheme: () => ({ setTheme: vi.fn(), theme: "light", toggleTheme: vi.fn() }),
}));
const { ThemeCommandPage } = await import("./theme");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
});
const renderPage = () =>
render(
<I18nProvider i18n={i18n}>
<Command>
<ThemeCommandPage />
</Command>
</I18nProvider>,
);
describe("ThemeCommandPage", () => {
it("is hidden when 'theme' is not on top of the page stack", () => {
renderPage();
expect(screen.queryByText("Light theme")).toBeNull();
});
it("renders Light + Dark options when active", () => {
useCommandPaletteStore.setState({ pages: ["theme"] });
renderPage();
expect(screen.getByText("Light theme")).toBeInTheDocument();
expect(screen.getByText("Dark theme")).toBeInTheDocument();
});
});
@@ -0,0 +1,30 @@
import { Trans } from "@lingui/react/macro";
import { MoonIcon, SunIcon } from "@phosphor-icons/react";
import { CommandItem } from "@reactive-resume/ui/components/command";
import { useTheme } from "@/features/theme/provider";
import { useCommandPaletteStore } from "../../store";
import { BaseCommandGroup } from "../base";
export function ThemeCommandPage() {
const { setTheme } = useTheme();
const setOpen = useCommandPaletteStore((state) => state.setOpen);
const handleThemeChange = (theme: "light" | "dark") => {
setTheme(theme, { playSound: false });
setOpen(false);
};
return (
<BaseCommandGroup page="theme" heading={<Trans>Theme</Trans>}>
<CommandItem value="light" onSelect={() => handleThemeChange("light")}>
<SunIcon />
<Trans>Light theme</Trans>
</CommandItem>
<CommandItem value="dark" onSelect={() => handleThemeChange("dark")}>
<MoonIcon />
<Trans>Dark theme</Trans>
</CommandItem>
</BaseCommandGroup>
);
}
@@ -0,0 +1,84 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { PlusIcon, ReadCvLogoIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { useNavigate, useRouteContext } from "@tanstack/react-router";
import { CommandLoading } from "cmdk";
import { CommandItem, CommandShortcut } from "@reactive-resume/ui/components/command";
import { Kbd } from "@reactive-resume/ui/components/kbd";
import { useDialogStore } from "@/dialogs/store";
import { orpc } from "@/libs/orpc/client";
import { useCommandPaletteStore } from "../store";
import { BaseCommandGroup } from "./base";
export function ResumesCommandGroup() {
const navigate = useNavigate();
const { openDialog } = useDialogStore();
const { session } = useRouteContext({ strict: false });
const reset = useCommandPaletteStore((state) => state.reset);
const peekPage = useCommandPaletteStore((state) => state.peekPage);
const pushPage = useCommandPaletteStore((state) => state.pushPage);
const isResumesPage = peekPage() === "resumes";
const { data: resumes, isLoading } = useQuery(
orpc.resume.list.queryOptions({
enabled: !!session && isResumesPage,
}),
);
const onCreate = async () => {
await navigate({ to: "/dashboard/resumes" });
openDialog("resume.create", undefined);
reset();
};
const onNavigate = async (path: string) => {
await navigate({ to: path });
reset();
};
if (!session) return null;
return (
<>
<BaseCommandGroup heading={<Trans>Search for...</Trans>}>
<CommandItem keywords={[t`Resumes`]} value="search.resumes" onSelect={() => pushPage("resumes")}>
<ReadCvLogoIcon />
<Trans>Resumes</Trans>
</CommandItem>
</BaseCommandGroup>
<BaseCommandGroup page="resumes" heading={<Trans>Resumes</Trans>}>
<CommandItem onSelect={onCreate}>
<PlusIcon />
<Trans>Create a new resume</Trans>
</CommandItem>
{isLoading ? (
<CommandLoading>
<Trans>Loading resumes...</Trans>
</CommandLoading>
) : (
resumes?.map((resume) => (
<CommandItem
key={resume.id}
value={resume.id}
keywords={[resume.name]}
onSelect={() => onNavigate(`/builder/${resume.id}`)}
>
<ReadCvLogoIcon />
{resume.name}
<CommandShortcut className="opacity-0 transition-opacity group-data-[selected=true]/command-item:opacity-100">
<Trans comment="Command palette hint that pressing Enter opens the selected resume">
Press <Kbd>Enter</Kbd> to open
</Trans>
</CommandShortcut>
</CommandItem>
))
)}
</BaseCommandGroup>
</>
);
}
@@ -0,0 +1,118 @@
import { afterEach, describe, expect, it } from "vitest";
import { useCommandPaletteStore } from "./store";
const reset = () => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
};
afterEach(reset);
describe("useCommandPaletteStore", () => {
it("starts closed with empty search and no pages", () => {
const state = useCommandPaletteStore.getState();
expect(state.open).toBe(false);
expect(state.search).toBe("");
expect(state.pages).toEqual([]);
});
it("setOpen(true) opens the palette without touching other fields", () => {
useCommandPaletteStore.setState({ search: "foo", pages: ["page1"] });
useCommandPaletteStore.getState().setOpen(true);
const state = useCommandPaletteStore.getState();
expect(state.open).toBe(true);
expect(state.search).toBe("foo");
expect(state.pages).toEqual(["page1"]);
});
it("setOpen(false) resets the entire store back to initial state", () => {
useCommandPaletteStore.setState({ open: true, search: "foo", pages: ["page1"] });
useCommandPaletteStore.getState().setOpen(false);
const state = useCommandPaletteStore.getState();
expect(state.open).toBe(false);
expect(state.search).toBe("");
expect(state.pages).toEqual([]);
});
it("setSearch updates only the search field", () => {
useCommandPaletteStore.getState().setSearch("query");
expect(useCommandPaletteStore.getState().search).toBe("query");
});
it("pushPage appends a page and clears search", () => {
useCommandPaletteStore.setState({ search: "leftover" });
useCommandPaletteStore.getState().pushPage("settings");
const state = useCommandPaletteStore.getState();
expect(state.pages).toEqual(["settings"]);
expect(state.search).toBe("");
});
it("pushPage stacks multiple pages in order", () => {
const { pushPage } = useCommandPaletteStore.getState();
pushPage("a");
pushPage("b");
pushPage("c");
expect(useCommandPaletteStore.getState().pages).toEqual(["a", "b", "c"]);
});
it("peekPage returns the top page (or undefined when empty)", () => {
expect(useCommandPaletteStore.getState().peekPage()).toBeUndefined();
useCommandPaletteStore.setState({ pages: ["a", "b"] });
expect(useCommandPaletteStore.getState().peekPage()).toBe("b");
});
it("popPage removes the last page and clears search", () => {
useCommandPaletteStore.setState({ pages: ["a", "b"], search: "x" });
useCommandPaletteStore.getState().popPage();
const state = useCommandPaletteStore.getState();
expect(state.pages).toEqual(["a"]);
expect(state.search).toBe("");
});
it("reset clears every state field", () => {
useCommandPaletteStore.setState({ open: true, search: "x", pages: ["a"] });
useCommandPaletteStore.getState().reset();
expect(useCommandPaletteStore.getState()).toMatchObject({ open: false, search: "", pages: [] });
});
describe("goBack", () => {
it("clears search first if present, leaving pages and open untouched", () => {
useCommandPaletteStore.setState({ open: true, search: "text", pages: ["a"] });
useCommandPaletteStore.getState().goBack();
const state = useCommandPaletteStore.getState();
expect(state.search).toBe("");
expect(state.pages).toEqual(["a"]);
expect(state.open).toBe(true);
});
it("pops the top page when no search and pages exist", () => {
useCommandPaletteStore.setState({ open: true, search: "", pages: ["a", "b"] });
useCommandPaletteStore.getState().goBack();
const state = useCommandPaletteStore.getState();
expect(state.pages).toEqual(["a"]);
expect(state.open).toBe(true);
});
it("closes the palette when no search and no pages", () => {
useCommandPaletteStore.setState({ open: true, search: "", pages: [] });
useCommandPaletteStore.getState().goBack();
expect(useCommandPaletteStore.getState().open).toBe(false);
});
});
});
@@ -0,0 +1,59 @@
import { create } from "zustand/react";
type CommandPaletteState = {
open: boolean;
search: string;
pages: string[];
};
type CommandPaletteActions = {
setOpen: (open: boolean) => void;
setSearch: (search: string) => void;
pushPage: (page: string) => void;
peekPage: () => string | undefined;
popPage: () => void;
reset: () => void;
goBack: () => void;
};
type CommandPaletteStore = CommandPaletteState & CommandPaletteActions;
const initialState: CommandPaletteState = {
open: false,
search: "",
pages: [],
};
export const useCommandPaletteStore = create<CommandPaletteStore>((set, get) => ({
...initialState,
setOpen: (open) => {
set({ open });
if (!open) set(initialState);
},
setSearch: (search) => set({ search }),
peekPage: () => get().pages[get().pages.length - 1],
pushPage: (page) => set((state) => ({ pages: [...state.pages, page], search: "" })),
popPage: () => set((state) => ({ pages: state.pages.slice(0, -1), search: "" })),
reset: () => set(initialState),
goBack: () => {
set((state) => {
if (state.search.length > 0) {
// If there's text, just clear the search
return { search: "" };
}
if (state.pages.length > 0) {
// If on a sub-page, go back
return { pages: state.pages.slice(0, -1), search: "" };
}
// If on first page, close
return { open: false, search: "", pages: [] };
});
},
}));