688a18af22
Foundation for the mobile-responsive-parity change. Adds: - useIsMobile() hook: single source of truth for the md:768px cut (SSR-safe) - MobileCardRow<T>: stacked card list for wide tables below md, with getRowId stable keys, primary field as title, optional onRowClick + actions slot - SheetForm: full-height form host (h-[100dvh], flex column, sticky header + footer via flex not position:sticky) for mobile edit flows - HoverEditButton: mobile prop (default 'always') -- always visible below md, hover-revealed at md+; desktop aesthetic preserved - .mobile-touch-target CSS utility: 44x44 min hit area below md (WCAG 2.5.5) - App.tsx refactored to use useIsMobile(); shell behavior unchanged Tests cover primary/field rendering, onRowClick, actions slot, empty rows, no-primary, stable keys (no duplicate-key warning), and all SheetForm interactions. 86 tests pass; lint/build green. MobileCardRow key strategy: uses getRowId when provided (falls back to index); per design §trade-offs, fields are declared per-table to prioritize by mobile importance rather than auto-derived from column defs. Refs openspec/changes/mobile-responsive-parity/ (design §Shared primitives, spec R1/R5/R6, tasks slice 1).
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
import { Pencil } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
interface HoverEditButtonProps {
|
|
onClick: () => void;
|
|
label?: string;
|
|
/** Controls visibility below the `md:` (768px) breakpoint.
|
|
*
|
|
* - `always` (default): the button is always visible on mobile/touch.
|
|
* - `hover`: keep the legacy opacity-0-everywhere behavior.
|
|
*
|
|
* At `md:` and above the hover-reveal aesthetic is always preserved
|
|
* (`md:opacity-0 md:group-hover:opacity-100`), so desktop is not regressed.
|
|
* See OpenSpec change `mobile-responsive-parity`, spec R5. */
|
|
mobile?: "always" | "hover";
|
|
}
|
|
|
|
/**
|
|
* Hover-to-reveal edit affordance (desktop) / always-visible (mobile).
|
|
*
|
|
* Keeps the `rail-edit` class plus the opacity base + transition so the
|
|
* existing hover-reveal rules in consuming pages (Actions, Settings) still
|
|
* target it (`&:hover .rail-edit { opacity: 1 }`) until those pages migrate.
|
|
*
|
|
* Mobile behavior (`mobile="always"`, the default): the button is visible by
|
|
* default below `md` because hover does not fire on touch. The hover-reveal
|
|
* aesthetic is layered back on at `md:` and above via `md:opacity-0
|
|
* md:group-hover:opacity-100`. MUI IconButton + EditOutlined → shadcn `Button
|
|
* variant="ghost" size="icon-sm"` + lucide `Pencil`. Same exported props/display
|
|
* name. See OpenSpec change `mobile-responsive-parity`, spec R5.
|
|
*/
|
|
export function HoverEditButton({
|
|
onClick,
|
|
label = "Edit",
|
|
mobile = "always",
|
|
}: HoverEditButtonProps) {
|
|
// Legacy mode: opacity-0 everywhere, revealed by group hover (the consuming
|
|
// row supplies `group`).
|
|
const hoverClasses =
|
|
mobile === "hover"
|
|
? "opacity-0 transition-opacity duration-100 ease-out group-hover:opacity-100"
|
|
: "md:opacity-0 md:transition-opacity md:duration-100 md:ease-out md:group-hover:opacity-100";
|
|
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
className={`rail-edit text-muted-foreground mobile-touch-target ${hoverClasses}`}
|
|
aria-label={label}
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onClick();
|
|
}}
|
|
>
|
|
<Pencil />
|
|
</Button>
|
|
);
|
|
}
|