Add Phase 1 Pocket Pascal UI: home, pantry, week plan, and plate.
Replace the hello-world shell with the elevated design system, bilingual screens, recipe/extras data, and the interactive Mein Teller view. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
112
handover/component-library/components/Button.tsx
Normal file
112
handover/component-library/components/Button.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
||||
import { cx } from "../lib/cx";
|
||||
|
||||
export type ButtonVariant = "primary" | "secondary" | "fab";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
loading?: boolean;
|
||||
/** Required for `variant="fab"` — icon-only, no visible label. */
|
||||
icon?: ReactNode;
|
||||
}
|
||||
|
||||
function Spinner({ dark = false }: { dark?: boolean }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cx(
|
||||
"inline-block h-4 w-4 animate-spin rounded-full border-2",
|
||||
dark ? "border-[var(--ink)]/25 border-t-[var(--ink)]" : "border-white/40 border-t-white"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared press behavior: 1px downward shift + swap from elevated to an
|
||||
* inset shadow + slight darkening. This is what actually reads as
|
||||
* "physically pushed in" — scale alone doesn't sell it, the inset shadow
|
||||
* does the work.
|
||||
*/
|
||||
const pressable = cx(
|
||||
"elevation-transition active:translate-y-px active:brightness-95",
|
||||
"shadow-[var(--shadow-elevated)] active:shadow-[var(--shadow-pressed)]"
|
||||
);
|
||||
|
||||
const base = cx(
|
||||
"relative inline-flex items-center justify-center gap-2",
|
||||
"font-body font-bold no-select [-webkit-tap-highlight-color:transparent]",
|
||||
"disabled:opacity-40 disabled:pointer-events-none disabled:active:translate-y-0 disabled:active:shadow-[var(--shadow-elevated)]"
|
||||
);
|
||||
|
||||
/**
|
||||
* Touch-optimized button set. All variants meet the 48px minimum touch target.
|
||||
*
|
||||
* Text color rule: our brand action gradients (lime→teal, coral→amber) are
|
||||
* light/high-luminance by design — see theme.css. Buttons use --ink text on
|
||||
* them, not white, to stay at WCAG AA without a text-shadow crutch. The
|
||||
* `secondary` variant uses --ink for the same reason (low-transparency
|
||||
* surface fill, not a dark background).
|
||||
*/
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = "primary", loading = false, disabled, icon, className, children, ...props }, ref) => {
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
if (variant === "fab") {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
className={cx(base, pressable, "h-14 w-14 rounded-full text-[var(--ink)]", className)}
|
||||
style={{ backgroundImage: "var(--gradient-accent)" }}
|
||||
{...props}
|
||||
>
|
||||
{loading ? <Spinner dark /> : icon ?? children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === "secondary") {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
className={cx(
|
||||
base,
|
||||
pressable,
|
||||
"min-h-[48px] overflow-hidden rounded-2xl px-6 text-[var(--ink)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="absolute inset-0 rounded-2xl"
|
||||
style={{
|
||||
background: "var(--glass-fill)",
|
||||
backdropFilter: "blur(var(--glass-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--glass-blur))",
|
||||
}}
|
||||
/>
|
||||
<span className="glass-border pointer-events-none absolute inset-0 rounded-2xl" />
|
||||
<span className="relative z-10 flex items-center gap-2">
|
||||
{loading ? <Spinner dark /> : children}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// primary
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
className={cx(base, pressable, "min-h-[48px] rounded-2xl px-6 text-[var(--ink)]", className)}
|
||||
style={{ backgroundImage: "var(--gradient-primary)" }}
|
||||
{...props}
|
||||
>
|
||||
{loading ? <Spinner dark /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
Reference in New Issue
Block a user