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.
63 lines
2.3 KiB
TypeScript
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>
|
|
);
|
|
}
|