Files
siftlode/frontend/src/components/PaletteLauncher.tsx
T

70 lines
2.8 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Command } from "lucide-react";
import { SHORTCUTS, formatChord, openPalette } from "../lib/keymap";
import { Pressable } from "./ui/Pressable";
import clsx from "clsx";
import { fieldClass } from "./ui/fieldStyles";
// 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"),
});
}
// Each accessible name CONTAINS what the control shows (WCAG 2.5.3, Label in Name): a voice-control
// user says what they see — "click Jump to", "click Ctrl K" — and a name that replaced the visible
// words matched nothing.
/** 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 (
<Pressable
onClick={openPalette}
aria-label={`${t("palette.open")} (${cap})`}
title={`${t("palette.open")} (${cap})`}
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}
</Pressable>
);
}
/** The same entry for a page whose header has no search field. */
export function PaletteLauncher() {
const { t } = useTranslation();
const cap = useCap();
return (
<Pressable
onClick={openPalette}
title={t("palette.open")}
// The search box's own frame (a 40 glass pill with the field edge): on a page without a search
// this is where the search would be, and it opens the same place.
className={clsx(
fieldClass({ size: "lg", pill: true, floating: true }),
"pointer-events-auto flex items-center gap-2 text-muted hover:text-fg",
)}
>
<Command className="h-4 w-4 shrink-0" aria-hidden="true" />
<span>{t("palette.title")}</span>
<span className="rounded-md border border-border px-1.5 py-0.5 font-mono text-3xs @max-[640px]/appmain:hidden">
{cap}
</span>
</Pressable>
);
}