mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-02 17:54:22 +10:00
feat(web): mark the active phone tab with a bar that springs between tabs
The phone tab bars (resume builder, letter builder, dashboard) drop the pill behind the active icon for a short accent bar on the tab's top edge. Switching tabs springs it across with a little overshoot (spring 0.4s, bounce 0.3); reduced motion moves it at once. Dashboard tab taps swap the page without the cross-fade, like a native tab bar, so the bar's movement isn't hidden behind the page transition.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { m } from "motion/react";
|
||||
import { TAB_SPRING } from "@/libs/motion";
|
||||
|
||||
/**
|
||||
* The phone tab bar's marker: a short accent bar on the active tab's top edge. Render it inside the active tab
|
||||
* (which must be `relative`); when another tab becomes active it springs across to it. Reduced motion moves it at
|
||||
* once (see `followReducedMotion`).
|
||||
*/
|
||||
export function MobileTabIndicator() {
|
||||
return (
|
||||
<m.span
|
||||
aria-hidden="true"
|
||||
layoutId="mobile-tab-indicator"
|
||||
transition={TAB_SPRING}
|
||||
className="absolute inset-x-0 -top-px mx-auto h-0.5 w-8 rounded-full bg-accent"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,9 @@ export const EXIT = 0.7;
|
||||
/** --ease-out-strong. Landing page only (features/homepage/site-footer.tsx); app code uses EASE. */
|
||||
export const EASE_OUT_STRONG = [0.23, 1, 0.32, 1] as const;
|
||||
|
||||
/** Springy, a little elastic: the mobile tab bar's marker hopping to the new tab (interruptible mid-flight). */
|
||||
export const TAB_SPRING = { type: "spring", duration: 0.4, bounce: 0.3 } as const;
|
||||
|
||||
/** dnd-kit timing for things settling into place (sortable shuffles, drop animations): on-screen movement. */
|
||||
export const DRAG_SETTLE = { duration: D2 * 1000, easing: `cubic-bezier(${EASE_MOVE.join(", ")})` };
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Icon } from "@reactive-resume/ui/components/icon";
|
||||
import { Tabs, TabsContent } from "@reactive-resume/ui/components/tabs";
|
||||
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { MobileTabIndicator } from "@/components/layout/mobile-tab-indicator";
|
||||
import {
|
||||
AssistantColumn,
|
||||
AssistantOverlay,
|
||||
@@ -285,18 +286,12 @@ function MobileBody({ mode, onModeChange }: MobileBodyProps) {
|
||||
if (tab !== "page") onModeChange(tab);
|
||||
}}
|
||||
className={cn(
|
||||
"flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px]",
|
||||
"relative flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px] transition-[color,scale] duration-quick ease-enter active:scale-[0.97]",
|
||||
active ? "font-semibold text-ink" : "text-ink-2",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-7 w-[52px] items-center justify-center rounded-full transition-colors duration-quick",
|
||||
active && "bg-accent-soft text-accent-text",
|
||||
)}
|
||||
>
|
||||
<Icon name={icon} size={24} filled={active} />
|
||||
</span>
|
||||
{active && <MobileTabIndicator />}
|
||||
<Icon name={icon} size={24} filled={active} />
|
||||
{labels[tab]}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Tabs, TabsContent } from "@reactive-resume/ui/components/tabs";
|
||||
import { toast } from "@reactive-resume/ui/components/toast";
|
||||
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { MobileTabIndicator } from "@/components/layout/mobile-tab-indicator";
|
||||
import {
|
||||
AssistantColumn,
|
||||
AssistantOverlay,
|
||||
@@ -213,18 +214,12 @@ function MobileBody({ mode, onModeChange }: MobileBodyProps) {
|
||||
if (tab !== "page") onModeChange(tab);
|
||||
}}
|
||||
className={cn(
|
||||
"flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px]",
|
||||
"relative flex min-h-[52px] flex-1 flex-col items-center justify-center gap-0.5 text-[11px] transition-[color,scale] duration-quick ease-enter active:scale-[0.97]",
|
||||
active ? "font-semibold text-ink" : "text-ink-2",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-7 w-[52px] items-center justify-center rounded-full transition-colors duration-quick",
|
||||
active && "bg-accent-soft text-accent-text",
|
||||
)}
|
||||
>
|
||||
<Icon name={icon} size={24} filled={active} />
|
||||
</span>
|
||||
{active && <MobileTabIndicator />}
|
||||
<Icon name={icon} size={24} filled={active} />
|
||||
{labels[tab]}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/com
|
||||
import { useBreakpoint } from "@reactive-resume/ui/hooks/use-breakpoint";
|
||||
import { getInitials } from "@reactive-resume/utils/string";
|
||||
import { cn } from "@reactive-resume/utils/style";
|
||||
import { MobileTabIndicator } from "@/components/layout/mobile-tab-indicator";
|
||||
import { useDialogStore } from "@/dialogs/store";
|
||||
import { applicationsListQueryOptions } from "@/features/applications/queries";
|
||||
import { useCommandPaletteStore } from "@/features/command-palette/store";
|
||||
@@ -289,6 +290,7 @@ function MobileTabs() {
|
||||
|
||||
const tab = (icon: IconName, label: string, current: boolean) => (
|
||||
<>
|
||||
{current && <MobileTabIndicator />}
|
||||
<Icon name={icon} size={24} filled={current} />
|
||||
<span className={cn("text-[11px]", current ? "font-semibold text-ink" : "text-ink-2")}>{label}</span>
|
||||
</>
|
||||
@@ -302,14 +304,16 @@ function MobileTabs() {
|
||||
<Link
|
||||
to="/dashboard"
|
||||
aria-current={isCurrent("/dashboard") ? "page" : undefined}
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
viewTransition={false}
|
||||
className="relative flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
>
|
||||
{tab("description", t`Documents`, isCurrent("/dashboard"))}
|
||||
</Link>
|
||||
<Link
|
||||
to="/dashboard/applications"
|
||||
aria-current={isCurrent("/dashboard/applications") ? "page" : undefined}
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
viewTransition={false}
|
||||
className="relative flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
>
|
||||
{tab("work", t`Applications`, isCurrent("/dashboard/applications"))}
|
||||
</Link>
|
||||
@@ -329,7 +333,8 @@ function MobileTabs() {
|
||||
<Link
|
||||
to="/dashboard/settings"
|
||||
aria-current={isCurrent("/dashboard/settings") ? "page" : undefined}
|
||||
className="flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
viewTransition={false}
|
||||
className="relative flex min-h-[52px] flex-col items-center justify-center gap-0.5 transition-[scale] duration-quick ease-enter active:scale-[0.97]"
|
||||
>
|
||||
{tab("account_circle", t`Account`, isCurrent("/dashboard/settings"))}
|
||||
</Link>
|
||||
|
||||
Reference in New Issue
Block a user