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>
);
}
|