fix(desktop): macos quit flash, first click and press feedback

This commit is contained in:
知食分子 authored and GitHub committed 2026-09-01 01:19:47 +08:00
1 parent 23f511de22
commit 554be11aea
5 files changed
+91 -11

No files matched your search

@@ -0,0 +1,43 @@
// Source-contract guard for shared button press feedback.
// Overview: desktop click responsiveness depends on buttons giving immediate
// visual feedback while pressed. DOM `click` only fires on pointer-up, so a
// press with zero visual delta reads as "the click did nothing" and users
// retry or hold the button (~1s holds measured in click-probe sessions).
//
// Two guarantees keep that regression from re-appearing:
// 1. Every button variant styles an `active:` pressed state, so pressing
// always changes something on screen before pointer-up.
// 2. The pressed state must not move or scale the button (`scale-*` /
// `translate-*`). A moving hit target can land pointer-up outside the
// element it was pressed on, which genuinely drops the click.
// Keep these assertions updated alongside the variant class strings.
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const buttonVariantsSource = readFileSync(new URL("../index.ts", import.meta.url), "utf8");
const VARIANTS = ["default", "outline", "secondary", "ghost", "destructive", "link"] as const;
function variantClasses(variant: string): string {
const match = buttonVariantsSource.match(new RegExp(`${variant}:\\s*"([^"]+)"`));
expect(match, `variant "${variant}" exists in buttonVariants`).toBeTruthy();
return match![1];
}
describe("button press feedback guard", () => {
it.each(VARIANTS)("styles an active pressed state for the %s variant", (variant) => {
expect(variantClasses(variant)).toMatch(/(^|\s)active:/);
});
it.each(VARIANTS)("does not move the hit target while %s is pressed", (variant) => {
expect(variantClasses(variant)).not.toMatch(/active:(scale|translate|-translate)-/);
});
it("does not move the hit target from the shared base classes", () => {
const base = buttonVariantsSource.match(/cva\(\s*"([^"]+)"/);
expect(base).toBeTruthy();
expect(base![1]).not.toMatch(/active:(scale|translate|-translate)-/);
});
});
@@ -8,12 +8,12 @@ export const buttonVariants = cva(
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
link: "text-primary underline-offset-4 hover:underline",
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80 [a]:active:bg-primary/90 active:bg-primary/90",
outline: "border-border bg-background hover:bg-muted hover:text-foreground active:bg-accent active:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 active:bg-secondary aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost: "hover:bg-muted hover:text-foreground active:bg-accent active:text-accent-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
destructive: "bg-destructive/10 hover:bg-destructive/20 active:bg-destructive/30 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30 dark:active:bg-destructive/40",
link: "text-primary underline-offset-4 hover:underline active:opacity-80",
},
size: {
default: "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",