Files
siftlode/frontend/src/components/PaletteLauncher.tsx
T
peter db8ed5a004 feat(ui): a visible way into the command palette
Ctrl/Cmd+K and the shortcut sheet had no entry point but a palette row, behind the other
undiscoverable chord. Owner's pick from the S7 mockups (2026-09-19): a key cap at the end of the
header's search field opens the palette, and a page with no search field shows a small "Jump to…"
pill with the same key cap in its place, so the entry is on every page. The cap reads the chord the
palette row binds from the keymap table (Ctrl K, or ⌘ K on a Mac) and stays out of a phone's
header, where there is no keyboard to press it on.
2026-09-19 17:58:39 +02:00

63 lines
2.3 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Command } from "lucide-react";
import { SHORTCUTS, formatChord, openPalette } from "../lib/keymap";
// The visible way into the command palette (and, from there, the shortcut sheet). Ctrl/Cmd+K and
// `h` had no entry point anywhere but a palette row — the one in-app pointer to the cheat sheet sat
// behind the other undiscoverable chord. The owner's pick (2026-09-19): a key cap where you already
// type, the header's search field; a page with no search field gets the same key cap on a small
// "Jump to…" pill in its place, so the entry is on every page.
/** The chord the palette row actually binds, read from the table rather than restated. */
const paletteChord = SHORTCUTS.find((s) => s.id === "palette")?.keys[0] ?? "Mod+k";
function useCap(): string {
const { t } = useTranslation();
return formatChord(paletteChord, {
Space: t("keys.cap.space"),
ContextMenu: t("keys.cap.menu"),
});
}
/** The key cap at the end of a search field. A button, so the pointer and a screen reader reach it
* too; left out of a phone's narrow header, where there is no keyboard to press it on. */
export function PaletteKeycap() {
const { t } = useTranslation();
const cap = useCap();
return (
<button
type="button"
onClick={openPalette}
aria-label={t("palette.open")}
title={t("palette.open")}
className="shrink-0 rounded-md border border-border px-1.5 py-0.5 font-mono text-3xs text-muted transition hover:border-accent/50 hover:text-fg @max-[640px]/appmain:hidden"
>
{cap}
</button>
);
}
/** The same entry for a page whose header has no search field. */
export function PaletteLauncher() {
const { t } = useTranslation();
const cap = useCap();
return (
<button
type="button"
onClick={openPalette}
aria-label={t("palette.open")}
className="pointer-events-auto glass-menu flex h-11 items-center gap-2 rounded-2xl px-3 text-sm text-muted transition hover:text-fg"
>
<Command className="h-4 w-4 shrink-0" aria-hidden="true" />
<span>{t("palette.title")}</span>
<span
aria-hidden="true"
className="rounded-md border border-border px-1.5 py-0.5 font-mono text-3xs @max-[640px]/appmain:hidden"
>
{cap}
</span>
</button>
);
}