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 { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { AppBottomNav } from "../app-bottom-nav";
|
||||||
import { Checkbox } from "../checkbox";
|
import { Checkbox } from "../checkbox";
|
||||||
import { LoadingIndicator } from "../loading-indicator";
|
import { LoadingIndicator } from "../loading-indicator";
|
||||||
import { PageLoadingFallback } from "../page-loading-fallback";
|
import { PageLoadingFallback } from "../page-loading-fallback";
|
||||||
@@ -73,4 +74,32 @@ describe("core Tailwind components", () => {
|
|||||||
expect(html).toContain("Content");
|
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 {
|
.root {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: calc(var(--app-safe-bottom, 0px) + 12px);
|
bottom: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 8px;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: min(
|
width: min(100vw, var(--app-max-width, 540px));
|
||||||
calc(100vw - var(--app-safe-left, 0px) - var(--app-safe-right, 0px) - 28px),
|
min-height: calc(
|
||||||
calc(var(--app-max-width, 540px) - 28px)
|
var(--app-bottom-nav-height, 64px) + var(--app-safe-bottom, 0px)
|
||||||
);
|
);
|
||||||
margin: 0 auto;
|
padding:
|
||||||
padding: 8px;
|
6px
|
||||||
border: 1px solid var(--nav-border);
|
calc(8px + var(--app-safe-right, 0px))
|
||||||
border-radius: 28px;
|
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);
|
background: var(--nav-background);
|
||||||
box-shadow: var(--nav-shadow);
|
box-shadow: var(--nav-shadow);
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
backdrop-filter: blur(20px) saturate(1.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.warm {
|
.warm {
|
||||||
--nav-background:
|
--nav-background: #fffaf7;
|
||||||
linear-gradient(135deg, rgba(255, 255, 255, 0.9), rgba(255, 240, 230, 0.8)),
|
--nav-border: rgba(70, 46, 55, 0.12);
|
||||||
rgba(255, 255, 255, 0.78);
|
--nav-shadow: 0 -6px 18px rgba(70, 46, 55, 0.08);
|
||||||
--nav-border: rgba(255, 116, 159, 0.18);
|
--nav-button-color: #806f75;
|
||||||
--nav-shadow:
|
--nav-button-hover-background: rgba(246, 87, 160, 0.07);
|
||||||
0 20px 48px rgba(131, 72, 85, 0.16),
|
--nav-button-hover-color: #4a353d;
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
--nav-active-color: #f657a0;
|
||||||
--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-focus-color: #ff5d95;
|
--nav-focus-color: #ff5d95;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--nav-background:
|
--nav-background: #17121c;
|
||||||
linear-gradient(135deg, rgba(36, 22, 39, 0.78), rgba(255, 255, 255, 0.1)),
|
--nav-border: rgba(255, 255, 255, 0.14);
|
||||||
rgba(23, 16, 28, 0.72);
|
--nav-shadow: 0 -6px 20px rgba(0, 0, 0, 0.24);
|
||||||
--nav-border: rgba(255, 255, 255, 0.18);
|
--nav-button-color: rgba(255, 255, 255, 0.62);
|
||||||
--nav-shadow:
|
--nav-button-hover-background: rgba(255, 255, 255, 0.07);
|
||||||
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-button-hover-color: #ffffff;
|
--nav-button-hover-color: #ffffff;
|
||||||
--nav-active-background:
|
--nav-active-color: #ff8fc4;
|
||||||
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-focus-color: #ffffff;
|
--nav-focus-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 3px;
|
||||||
border: 1px solid transparent;
|
border: 0;
|
||||||
border-radius: 22px;
|
border-radius: 10px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--nav-button-color);
|
color: var(--nav-button-color);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: clamp(12px, 2.963vw, 14px);
|
font-size: clamp(11px, 2.407vw, 13px);
|
||||||
font-weight: 820;
|
font-weight: 760;
|
||||||
|
line-height: 1.1;
|
||||||
transition:
|
transition:
|
||||||
background 0.18s ease,
|
background 0.18s ease,
|
||||||
color 0.18s ease,
|
color 0.18s ease,
|
||||||
@@ -93,8 +81,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
border-color: var(--nav-active-border);
|
|
||||||
background: var(--nav-active-background);
|
|
||||||
color: var(--nav-active-color);
|
color: var(--nav-active-color);
|
||||||
box-shadow: var(--nav-active-shadow);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,16 @@
|
|||||||
padding:
|
padding:
|
||||||
0
|
0
|
||||||
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;
|
0;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 12% 10%, rgba(255, 193, 121, 0.32), transparent 30%),
|
radial-gradient(circle at 12% 10%, rgba(255, 255, 255, 0.82), transparent 32%),
|
||||||
radial-gradient(circle at 90% 4%, rgba(255, 91, 148, 0.22), transparent 28%),
|
radial-gradient(circle at 90% 4%, rgba(210, 214, 211, 0.3), transparent 30%),
|
||||||
linear-gradient(180deg, #fff7ed 0%, #fff8f6 42%, #ffffff 100%);
|
linear-gradient(180deg, #f3f4f2 0%, #f7f7f5 46%, #fbfbfa 100%);
|
||||||
color: #21171b;
|
color: #21171b;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
right: -82px;
|
right: -82px;
|
||||||
width: 170px;
|
width: 170px;
|
||||||
height: 170px;
|
height: 170px;
|
||||||
background: rgba(255, 93, 149, 0.16);
|
background: rgba(190, 195, 191, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.backgroundGlowTwo {
|
.backgroundGlowTwo {
|
||||||
@@ -36,7 +39,7 @@
|
|||||||
left: -92px;
|
left: -92px;
|
||||||
width: 190px;
|
width: 190px;
|
||||||
height: 190px;
|
height: 190px;
|
||||||
background: rgba(255, 188, 92, 0.18);
|
background: rgba(222, 224, 220, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero,
|
.hero,
|
||||||
@@ -518,7 +521,9 @@
|
|||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: max(calc((100vw - var(--app-max-width, 540px)) / 2 + 18px), 18px);
|
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);
|
left: max(calc((100vw - var(--app-max-width, 540px)) / 2 + 18px), 18px);
|
||||||
z-index: 45;
|
z-index: 45;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function PrivateRoomScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell background="#fff7ed">
|
<MobileShell background="#f3f4f2">
|
||||||
<main className={styles.shell}>
|
<main className={styles.shell}>
|
||||||
<div className={styles.backgroundGlowOne} />
|
<div className={styles.backgroundGlowOne} />
|
||||||
<div className={styles.backgroundGlowTwo} />
|
<div className={styles.backgroundGlowTwo} />
|
||||||
@@ -186,7 +186,7 @@ export function PrivateRoomScreen() {
|
|||||||
|
|
||||||
<AppBottomNav
|
<AppBottomNav
|
||||||
activeItem="privateRoom"
|
activeItem="privateRoom"
|
||||||
onChatClick={() => navigator.openChat({ replace: false })}
|
onChatClick={() => navigator.push(ROUTES.splash, { scroll: false })}
|
||||||
onPrivateRoomClick={() => undefined}
|
onPrivateRoomClick={() => undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ describe("splash Tailwind components", () => {
|
|||||||
expect(readyHtml).toContain("z-2");
|
expect(readyHtml).toContain("z-2");
|
||||||
expect(readyHtml).toContain("max-w-120");
|
expect(readyHtml).toContain("max-w-120");
|
||||||
expect(readyHtml).toContain("bg-[linear-gradient(to_right");
|
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(readyHtml).toContain("Start Chatting");
|
||||||
expect(loadingHtml).toContain("disabled");
|
expect(loadingHtml).toContain("disabled");
|
||||||
expect(loadingHtml).toContain("animate-spin");
|
expect(loadingHtml).toContain("animate-spin");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function SplashButton({ onStartChat }: SplashButtonProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onStartChat}
|
onClick={onStartChat}
|
||||||
disabled={isLoading}
|
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 ? (
|
{isLoading ? (
|
||||||
<LoadingIndicator color="#ffffff" />
|
<LoadingIndicator color="#ffffff" />
|
||||||
|
|||||||
@@ -14,16 +14,19 @@
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
touch-action: manipulation;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
inset 0 1px 0 rgba(255, 255, 255, 0.18),
|
||||||
0 18px 42px rgba(46, 22, 38, 0.18);
|
0 18px 42px rgba(46, 22, 38, 0.18);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
animation: messageFloatIn 0.5s ease 0.08s both;
|
animation: messageFloatIn 0.5s ease 0.08s backwards;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.18s ease,
|
border-color 0.18s ease,
|
||||||
|
filter 0.14s ease,
|
||||||
box-shadow 0.18s ease,
|
box-shadow 0.18s ease,
|
||||||
transform 0.18s ease;
|
transform 0.14s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
@@ -35,7 +38,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card:active {
|
.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 {
|
.card:focus-visible {
|
||||||
@@ -53,6 +61,11 @@
|
|||||||
background: linear-gradient(145deg, #ffb36d, #ff5d95);
|
background: linear-gradient(145deg, #ffb36d, #ff5d95);
|
||||||
box-shadow: 0 10px 24px rgba(255, 95, 128, 0.24);
|
box-shadow: 0 10px 24px rgba(255, 95, 128, 0.24);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
transition: transform 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:active .avatarWrap {
|
||||||
|
transform: scale(0.94);
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@@ -122,3 +135,19 @@
|
|||||||
background-position: 0 0;
|
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:
|
padding:
|
||||||
calc(var(--page-padding-y, 20px) + var(--app-safe-top, 0px))
|
calc(var(--page-padding-y, 20px) + var(--app-safe-top, 0px))
|
||||||
calc(var(--page-padding-x, 26px) + var(--app-safe-right, 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));
|
calc(var(--page-padding-x, 26px) + var(--app-safe-left, 0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ export function SplashScreen() {
|
|||||||
navigator.push(ROUTES.privateRoom, { scroll: false });
|
navigator.push(ROUTES.privateRoom, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenSplash = () => {
|
||||||
|
navigator.push(ROUTES.splash, { scroll: false });
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pwaUtil.prepareInstallPrompt();
|
pwaUtil.prepareInstallPrompt();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -69,9 +73,9 @@ export function SplashScreen() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AppBottomNav
|
<AppBottomNav
|
||||||
activeItem={null}
|
activeItem="chat"
|
||||||
variant="dark"
|
variant="dark"
|
||||||
onChatClick={handleStartChat}
|
onChatClick={handleOpenSplash}
|
||||||
onPrivateRoomClick={handleOpenPrivateRoom}
|
onPrivateRoomClick={handleOpenPrivateRoom}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user