🚸 feat: redesign AI toolbar with two-zone layout and HoverCard shortcuts popover

- Split composer into AI zone (primary accent) and data actions zone (neutral)
- Move Attach/Generate to labeled footer bar below textarea (left/right aligned)
- Add info icon with HoverCard (hover preview) + Popover (pinned click) showing identical keyboard shortcuts content using shadcn HoverCard to fix theme inconsistency vs Tooltip
- Expose imperative triggerRef on ImagePicker for keyboard shortcut access
- Wire TooltipProvider in root layout; install shadcn kbd and hover-card
- Unauthenticated state shows locked CTA with real sign-in button weight
- Add behavioral contract tests for footer bar, info trigger, and zone layout
This commit is contained in:
2026-04-08 13:08:36 -04:00
parent 650d1d5f95
commit 722c0f0f7d
8 changed files with 881 additions and 151 deletions

View File

@@ -0,0 +1,89 @@
// ---------------------------------------------------------------------------
// 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: ["esc"], label: "Clear prompt" },
];
// ─── Key resolution ───────────────────────────────────────────────────────────
const MAC_MAP: Record<string, string> = {
mod: "⌘",
shift: "⇧",
alt: "⌥",
enter: "↵",
esc: "Esc",
};
const OTHER_MAP: Record<string, string> = {
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";
}