All files / src/components/layout sticky-bottom-bar.tsx

100% Statements 1/1
100% Branches 5/5
100% Functions 1/1
100% Lines 1/1

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58                                                                                    5x                              
import type { ReactNode } from "react";
 
/**
 * `StickyBottomBar` — the commit bar for a full-screen form page. From the
 * approved mobile spec (`docs/wip/specs/2026-03-31-portal-mobile-ux-redesign.md`
 * §3: "Sticky bottom action bar for full-screen form pages — Save button
 * always visible") and unbuilt until now.
 *
 * What the mockup (`.context/mockups/mobile.html`, frame 2) fixes about it:
 *
 * - On a `MobileFullPage` this bar **replaces** the bottom nav rather than
 *   stacking above it. A full page owns the screen — that is the visible
 *   difference between a page and a drawer, and the reason the <6-fields rule
 *   exists at all.
 * - `pb-safe` means real `env(safe-area-inset-bottom)` **on top of** the bar's
 *   own padding, never instead of it. `py-3` is deliberate: `styles.css`
 *   carries the `.pb-safe.py-3` floor that makes that true, and
 *   `styles.test.ts` fails the build if a call site composes a bottom padding
 *   with no floor.
 * - It lives inside a `MobileFullPage` (`components/ui/mobile-full-page.tsx`),
 *   which owns the viewport and the modal contract; this owns the commit.
 * - Ordering is the caller's job, but the rule is fixed: a secondary action
 *   stays narrow, and the primary takes the remaining width (`flex-1`),
 *   because on a phone the primary action should be the thumb's default
 *   target.
 */
export type StickyBottomBarProps = {
	children: ReactNode;
	className?: string;
	/**
	 * Keep the mobile bottom nav visible underneath and sit above it. The
	 * default (`false`) is the full-page case, where the bar has the foot of
	 * the screen to itself.
	 */
	aboveBottomNav?: boolean;
};
 
export function StickyBottomBar({
	children,
	className,
	aboveBottomNav = false,
}: StickyBottomBarProps) {
	return (
		// One template literal, not `cn()`, on purpose: `src/styles.test.ts`
		// reads class attributes statically to check that every `pb-safe` call
		// site has a `--pb-safe-min` floor, and it cannot see inside a helper
		// call. A class it cannot read is a class nobody checks — which is how
		// `pb-safe` shipped undefined in four files in the first place.
		<div
			className={`fixed inset-x-0 z-50 flex items-center gap-2 border-t border-border-strong bg-background-elevated px-4 py-3 pb-safe ${
				aboveBottomNav ? "bottom-14" : "bottom-0"
			} ${className ?? ""}`}
		>
			{children}
		</div>
	);
}