2eb649eceb
Below md, the Users directory and the three Backups tables render as MobileCardRow cards: - UsersPage: display name primary; username/activity/email fields. Each card carries a selection checkbox (44px via mobile-touch-target) in the actions slot with stopPropagation so toggling selection does not open the drawer; card-body tap still opens the drawer. - BackupAlertsTable: alert message primary; severity/type/created fields; Acknowledge action preserved in actions slot. - BackupJobsTable: job name primary; source/schedule/last-status fields (joins latestRuns into a JobCardRow). - BackupRunsTable: run job_id primary; status/duration/size/started fields; status-filter Select renders above both layouts (preserved on mobile). Desktop (md+) is byte-for-byte identical for all four components -- the UsersPage diff is dominated by re-indenting the existing Table into the isMobile ternary else branch. Fix from Slice 5 review: MobileCardRow now renders the clickable card as <div role=button tabIndex=0> with Enter/Space keyboard handling instead of <button>, so nesting a Radix Checkbox (which renders a <button>) in the actions slot produces valid HTML. The desktop-parity argument for <button>-in-<button> did not hold (desktop rows are <tr>, not buttons). Cross-cutting: useIsMobile hardened with typeof window.matchMedia guard (safe in real browsers; only changes jsdom crash -> false). The file-local 900px compose hook was renamed useComposeViewport to avoid collision with the shared 768px useIsMobile. Tests: BackupJobsTable test file added (was untested), UsersPage mobile selection round-trip + stopPropagation, mobile card render across all four components. 105 tests pass; lint/build green. Refs openspec/changes/mobile-responsive-parity/ (spec R3, tasks slice 5).
39 lines
1.2 KiB
TypeScript
39 lines
1.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
|
|
/** Mobile breakpoint (must match Tailwind `md:` and the OpenSpec spec R1.2). */
|
|
const MOBILE_QUERY = "(max-width: 768px)";
|
|
|
|
/**
|
|
* Single source of truth for the mobile/desktop responsive cut.
|
|
*
|
|
* Returns `true` when the viewport matches `max-width: 768px` (phone portrait),
|
|
* `false` at `md:` and above. SSR-safe: returns `false` when `window` is
|
|
* undefined so server-rendered markup stays on the desktop path.
|
|
*
|
|
* Replaces the ad-hoc `window.matchMedia("(max-width: 768px)")` reads scattered
|
|
* across pages (App.tsx, Media.tsx) — see OpenSpec change
|
|
* `mobile-responsive-parity`, design §`useIsMobile`.
|
|
*/
|
|
export function useIsMobile(): boolean {
|
|
const [isMobile, setIsMobile] = useState(
|
|
() =>
|
|
typeof window !== "undefined" &&
|
|
typeof window.matchMedia === "function" &&
|
|
window.matchMedia(MOBILE_QUERY).matches,
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
typeof window === "undefined" ||
|
|
typeof window.matchMedia !== "function"
|
|
)
|
|
return;
|
|
const mql = window.matchMedia(MOBILE_QUERY);
|
|
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
|
mql.addEventListener("change", handler);
|
|
return () => mql.removeEventListener("change", handler);
|
|
}, []);
|
|
|
|
return isMobile;
|
|
}
|