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:
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user