// --------------------------------------------------------------------------- // Keyboard shortcuts – OS-aware key resolution // // Design: // - SHORTCUT_DEFINITIONS: abstract schema using modifier tokens // - resolveKeys(): pure function, safe to call anywhere (including tests) // - useOs(): client-only React hook that detects Mac vs other after hydration // // Modifier tokens: // "mod" → ⌘ (Mac) | Ctrl (other) // "shift" → ⇧ (Mac) | Shift (other) // "alt" → ⌥ (Mac) | Alt (other) // "enter" → ↵ (Mac) | Enter (other) // "esc" → Esc (both) // anything else passes through as-is (plain letter keys like "A") // --------------------------------------------------------------------------- export type Os = "mac" | "other" | "unknown"; export type Modifier = string; // "mod" | "shift" | "alt" | "enter" | "esc" | plain key export interface ShortcutDefinition { modifiers: readonly Modifier[]; label: string; } // ─── Abstract shortcut definitions ─────────────────────────────────────────── export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [ { modifiers: ["mod", "enter"], label: "Generate event" }, { modifiers: ["mod", "shift", "A"], label: "Attach image" }, { modifiers: ["mod", "V"], label: "Paste image" }, { modifiers: ["esc"], label: "Clear prompt" }, ]; // ─── Key resolution ─────────────────────────────────────────────────────────── const MAC_MAP: Record = { mod: "⌘", shift: "⇧", alt: "⌥", enter: "↵", esc: "Esc", }; const OTHER_MAP: Record = { mod: "Ctrl", shift: "Shift", alt: "Alt", enter: "Enter", esc: "Esc", }; /** * Pure function — maps abstract modifier tokens to display glyphs. * "unknown" falls back to Mac (most common dev/user base). */ export function resolveKeys(modifiers: readonly Modifier[], os: Os): string[] { const map = os === "other" ? OTHER_MAP : MAC_MAP; return modifiers.map((m) => map[m] ?? m); } // ─── OS detection hook (client-only) ───────────────────────────────────────── /** * Detects the user's OS after hydration. * Returns "unknown" on the server or before the effect runs. * * Detection order (most → least reliable): * 1. navigator.userAgentData.platform (modern browsers, Chromium) * 2. navigator.platform (legacy, still widely supported) * 3. navigator.userAgent string match (last resort) */ export function detectOs(): Os { if (typeof navigator === "undefined") return "unknown"; // Modern API — Chromium 90+ const uaData = (navigator as Navigator & { userAgentData?: { platform: string } }) .userAgentData; if (uaData?.platform) { return uaData.platform.toLowerCase().includes("mac") ? "mac" : "other"; } // Legacy API — still reliable on Safari and Firefox if (navigator.platform) { return navigator.platform.toLowerCase().startsWith("mac") ? "mac" : "other"; } // Last resort — UA string return /mac/i.test(navigator.userAgent) ? "mac" : "other"; }