feat(ui): refine home and private room navigation

Replace the floating bottom navigation with a full-width tab bar. Add splash press feedback and a neutral private room background.
This commit is contained in:
2026-07-13 18:16:17 +08:00
parent 225d232763
commit d8cd05b228
9 changed files with 120 additions and 62 deletions
@@ -1,6 +1,7 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { AppBottomNav } from "../app-bottom-nav";
import { Checkbox } from "../checkbox";
import { LoadingIndicator } from "../loading-indicator";
import { PageLoadingFallback } from "../page-loading-fallback";
@@ -73,4 +74,32 @@ describe("core Tailwind components", () => {
expect(html).toContain("Content");
});
it("renders AppBottomNav themes and active items accessibly", () => {
const chatHtml = renderToStaticMarkup(
<AppBottomNav
activeItem="chat"
variant="dark"
onChatClick={() => undefined}
onPrivateRoomClick={() => undefined}
/>,
);
const privateRoomHtml = renderToStaticMarkup(
<AppBottomNav
activeItem="privateRoom"
variant="warm"
onChatClick={() => undefined}
onPrivateRoomClick={() => undefined}
/>,
);
expect(chatHtml).toContain('aria-label="Primary navigation"');
expect(chatHtml).toMatch(/class="[^"]*dark[^"]*"/);
expect(chatHtml).toMatch(
/<button[^>]*aria-current="page"[^>]*>.*?<span>Chat<\/span>/,
);
expect(privateRoomHtml).toMatch(/class="[^"]*warm[^"]*"/);
expect(privateRoomHtml).toMatch(
/<button[^>]*aria-current="page"[^>]*>.*?<span>Elio Private room<\/span>/,
);
});
});
@@ -1,77 +1,65 @@
.root {
position: fixed;
bottom: calc(var(--app-safe-bottom, 0px) + 12px);
bottom: 0;
left: 50%;
z-index: 30;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
box-sizing: border-box;
width: min(
calc(100vw - var(--app-safe-left, 0px) - var(--app-safe-right, 0px) - 28px),
calc(var(--app-max-width, 540px) - 28px)
width: min(100vw, var(--app-max-width, 540px));
min-height: calc(
var(--app-bottom-nav-height, 64px) + var(--app-safe-bottom, 0px)
);
margin: 0 auto;
padding: 8px;
border: 1px solid var(--nav-border);
border-radius: 28px;
padding:
6px
calc(8px + var(--app-safe-right, 0px))
calc(6px + var(--app-safe-bottom, 0px))
calc(8px + var(--app-safe-left, 0px));
border: 0;
border-top: 1px solid var(--nav-border);
background: var(--nav-background);
box-shadow: var(--nav-shadow);
transform: translateX(-50%);
backdrop-filter: blur(20px) saturate(1.2);
}
.warm {
--nav-background:
linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 240, 230, 0.8)),
rgba(255, 255, 255, 0.78);
--nav-border: rgba(255, 116, 159, 0.18);
--nav-shadow:
0 20px 48px rgba(131, 72, 85, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.92);
--nav-button-color: #80686f;
--nav-button-hover-background: rgba(255, 116, 159, 0.1);
--nav-button-hover-color: #2d2024;
--nav-active-background: linear-gradient(135deg, #fff4ec, #ffe5f0);
--nav-active-border: rgba(255, 116, 159, 0.26);
--nav-active-color: #201417;
--nav-active-shadow: 0 12px 26px rgba(255, 116, 159, 0.16);
--nav-background: #fffaf7;
--nav-border: rgba(70, 46, 55, 0.12);
--nav-shadow: 0 -6px 18px rgba(70, 46, 55, 0.08);
--nav-button-color: #806f75;
--nav-button-hover-background: rgba(246, 87, 160, 0.07);
--nav-button-hover-color: #4a353d;
--nav-active-color: #f657a0;
--nav-focus-color: #ff5d95;
}
.dark {
--nav-background:
linear-gradient(135deg, rgba(36, 22, 39, 0.78), rgba(255, 255, 255, 0.1)),
rgba(23, 16, 28, 0.72);
--nav-border: rgba(255, 255, 255, 0.18);
--nav-shadow:
0 22px 52px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
--nav-button-color: rgba(255, 255, 255, 0.78);
--nav-button-hover-background: rgba(255, 255, 255, 0.12);
--nav-background: #17121c;
--nav-border: rgba(255, 255, 255, 0.14);
--nav-shadow: 0 -6px 20px rgba(0, 0, 0, 0.24);
--nav-button-color: rgba(255, 255, 255, 0.62);
--nav-button-hover-background: rgba(255, 255, 255, 0.07);
--nav-button-hover-color: #ffffff;
--nav-active-background:
linear-gradient(135deg, rgba(255, 255, 255, 0.22), rgba(255, 116, 159, 0.2));
--nav-active-border: rgba(255, 255, 255, 0.3);
--nav-active-color: #ffffff;
--nav-active-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
--nav-active-color: #ff8fc4;
--nav-focus-color: #ffffff;
}
.button {
display: flex;
min-height: 52px;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid transparent;
border-radius: 22px;
gap: 3px;
border: 0;
border-radius: 10px;
background: transparent;
color: var(--nav-button-color);
cursor: pointer;
font: inherit;
font-size: clamp(12px, 2.963vw, 14px);
font-weight: 820;
font-size: clamp(11px, 2.407vw, 13px);
font-weight: 760;
line-height: 1.1;
transition:
background 0.18s ease,
color 0.18s ease,
@@ -93,8 +81,5 @@
}
.active {
border-color: var(--nav-active-border);
background: var(--nav-active-background);
color: var(--nav-active-color);
box-shadow: var(--nav-active-shadow);
}
@@ -4,13 +4,16 @@
padding:
0
0
calc(var(--app-safe-bottom, 0px) + 96px)
calc(
var(--app-safe-bottom, 0px) + var(--app-bottom-nav-height, 64px) +
var(--page-section-gap, 18px)
)
0;
overflow: hidden auto;
background:
radial-gradient(circle at 12% 10%, rgba(255, 193, 121, 0.32), transparent 30%),
radial-gradient(circle at 90% 4%, rgba(255, 91, 148, 0.22), transparent 28%),
linear-gradient(180deg, #fff7ed 0%, #fff8f6 42%, #ffffff 100%);
radial-gradient(circle at 12% 10%, rgba(255, 255, 255, 0.82), transparent 32%),
radial-gradient(circle at 90% 4%, rgba(210, 214, 211, 0.3), transparent 30%),
linear-gradient(180deg, #f3f4f2 0%, #f7f7f5 46%, #fbfbfa 100%);
color: #21171b;
}
@@ -28,7 +31,7 @@
right: -82px;
width: 170px;
height: 170px;
background: rgba(255, 93, 149, 0.16);
background: rgba(190, 195, 191, 0.18);
}
.backgroundGlowTwo {
@@ -36,7 +39,7 @@
left: -92px;
width: 190px;
height: 190px;
background: rgba(255, 188, 92, 0.18);
background: rgba(222, 224, 220, 0.3);
}
.hero,
@@ -518,7 +521,9 @@
.toast {
position: fixed;
right: max(calc((100vw - var(--app-max-width, 540px)) / 2 + 18px), 18px);
bottom: calc(var(--app-safe-bottom, 0px) + 92px);
bottom: calc(
var(--app-safe-bottom, 0px) + var(--app-bottom-nav-height, 64px) + 12px
);
left: max(calc((100vw - var(--app-max-width, 540px)) / 2 + 18px), 18px);
z-index: 45;
padding: 14px 16px;
+2 -2
View File
@@ -80,7 +80,7 @@ export function PrivateRoomScreen() {
};
return (
<MobileShell background="#fff7ed">
<MobileShell background="#f3f4f2">
<main className={styles.shell}>
<div className={styles.backgroundGlowOne} />
<div className={styles.backgroundGlowTwo} />
@@ -186,7 +186,7 @@ export function PrivateRoomScreen() {
<AppBottomNav
activeItem="privateRoom"
onChatClick={() => navigator.openChat({ replace: false })}
onChatClick={() => navigator.push(ROUTES.splash, { scroll: false })}
onPrivateRoomClick={() => undefined}
/>
@@ -70,6 +70,9 @@ describe("splash Tailwind components", () => {
expect(readyHtml).toContain("z-2");
expect(readyHtml).toContain("max-w-120");
expect(readyHtml).toContain("bg-[linear-gradient(to_right");
expect(readyHtml).toContain("active:enabled:scale-96");
expect(readyHtml).toContain("active:enabled:brightness-90");
expect(readyHtml).toContain("touch-manipulation");
expect(readyHtml).toContain("Start Chatting");
expect(loadingHtml).toContain("disabled");
expect(loadingHtml).toContain("animate-spin");
+1 -1
View File
@@ -19,7 +19,7 @@ export function SplashButton({ onStartChat }: SplashButtonProps) {
type="button"
onClick={onStartChat}
disabled={isLoading}
className="inline-flex min-h-(--responsive-control-height,48px) w-full max-w-120 flex-auto cursor-pointer items-center justify-center rounded-(--radius-full,999px) border-0 bg-[linear-gradient(to_right,var(--color-button-gradient-start),var(--color-button-gradient-end))] px-[clamp(var(--spacing-lg,16px),5.556vw,30px)] font-bold italic text-white shadow-[0_0_10px_rgba(248,89,168,0.3)] disabled:cursor-not-allowed disabled:opacity-70"
className="inline-flex min-h-(--responsive-control-height,48px) w-full max-w-120 touch-manipulation flex-auto cursor-pointer items-center justify-center rounded-(--radius-full,999px) border-0 bg-[linear-gradient(to_right,var(--color-button-gradient-start),var(--color-button-gradient-end))] px-[clamp(var(--spacing-lg,16px),5.556vw,30px)] font-bold italic text-white shadow-[0_0_10px_rgba(248,89,168,0.3)] transition-[filter,box-shadow,transform] duration-150 ease-out [-webkit-tap-highlight-color:transparent] focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-white enabled:hover:-translate-y-px enabled:hover:brightness-105 enabled:hover:shadow-[0_8px_22px_rgba(248,89,168,0.42)] active:enabled:translate-y-0.5 active:enabled:scale-96 active:enabled:brightness-90 active:enabled:shadow-[0_2px_6px_rgba(248,89,168,0.24)] motion-reduce:transition-none motion-reduce:active:enabled:transform-none disabled:cursor-not-allowed disabled:opacity-70"
>
{isLoading ? (
<LoadingIndicator color="#ffffff" />
@@ -14,16 +14,19 @@
color: #ffffff;
cursor: pointer;
font: inherit;
-webkit-tap-highlight-color: transparent;
text-align: left;
touch-action: manipulation;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 18px 42px rgba(46, 22, 38, 0.18);
backdrop-filter: blur(18px);
animation: messageFloatIn 0.5s ease 0.08s both;
animation: messageFloatIn 0.5s ease 0.08s backwards;
transition:
border-color 0.18s ease,
filter 0.14s ease,
box-shadow 0.18s ease,
transform 0.18s ease;
transform 0.14s ease;
}
.card:hover {
@@ -35,7 +38,12 @@
}
.card:active {
transform: translateY(1px) scale(0.995);
border-color: rgba(255, 255, 255, 0.48);
filter: brightness(0.9);
box-shadow:
inset 0 2px 8px rgba(22, 10, 20, 0.28),
0 6px 16px rgba(28, 12, 24, 0.18);
transform: translateY(2px) scale(0.97);
}
.card:focus-visible {
@@ -53,6 +61,11 @@
background: linear-gradient(145deg, #ffb36d, #ff5d95);
box-shadow: 0 10px 24px rgba(255, 95, 128, 0.24);
overflow: hidden;
transition: transform 0.14s ease;
}
.card:active .avatarWrap {
transform: scale(0.94);
}
.avatar {
@@ -122,3 +135,19 @@
background-position: 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
.card,
.avatarWrap {
transition-duration: 0.01ms;
}
.card {
animation: none;
}
.card:active,
.card:active .avatarWrap {
transform: none;
}
}
@@ -31,7 +31,10 @@
padding:
calc(var(--page-padding-y, 20px) + var(--app-safe-top, 0px))
calc(var(--page-padding-x, 26px) + var(--app-safe-right, 0px))
calc(var(--spacing-md, 12px) + var(--app-safe-bottom, 0px) + 86px)
calc(
var(--spacing-md, 12px) + var(--app-safe-bottom, 0px) +
var(--app-bottom-nav-height, 64px)
)
calc(var(--page-padding-x, 26px) + var(--app-safe-left, 0px));
}
+6 -2
View File
@@ -40,6 +40,10 @@ export function SplashScreen() {
navigator.push(ROUTES.privateRoom, { scroll: false });
};
const handleOpenSplash = () => {
navigator.push(ROUTES.splash, { scroll: false });
};
useEffect(() => {
pwaUtil.prepareInstallPrompt();
}, []);
@@ -69,9 +73,9 @@ export function SplashScreen() {
</p>
</div>
<AppBottomNav
activeItem={null}
activeItem="chat"
variant="dark"
onChatClick={handleStartChat}
onChatClick={handleOpenSplash}
onPrivateRoomClick={handleOpenPrivateRoom}
/>
</div>