feat(odin): chat animations + immersive fullscreen mobile view
- bubbles spring in anchored at their avatar corner; thinking state is a typing bubble (three bouncing dots) that springs in/out where the reply lands; reduced-motion falls back to opacity fades - mobile /odin is immersive: AppShell hides its header and main padding below md, the chat owns 100svh full-bleed with safe-area insets and a back arrow (history back, home on deep links) - desktop card border kept divider-colored via longhand border props (a responsive border shorthand reset the color to black) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
@@ -109,6 +110,17 @@ export default function OdinChat() {
|
||||
});
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
// Mobile is immersive (no AppShell header on /odin) — this is the only way
|
||||
// back. A deep link straight to /odin has no in-app history → go home.
|
||||
const navigate = useNavigate();
|
||||
const goBack = () => {
|
||||
if (((window.history.state as { idx?: number } | null)?.idx ?? 0) > 0) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
};
|
||||
|
||||
// No auto-select: like claude.ai, we land on a fresh "new chat" (activeId
|
||||
// null) and the sidebar lists existing conversations to open. A conversation
|
||||
// row in the DB is created lazily on the first message (see submit), so
|
||||
@@ -424,11 +436,16 @@ export default function OdinChat() {
|
||||
// browser-chrome height. svh sizes for the bar-visible viewport — the
|
||||
// page never scrolls and the chat's message list scrolls internally.
|
||||
// Desktop: svh === vh.
|
||||
height: "calc(100svh - 100px)",
|
||||
// Mobile is immersive (AppShell hides its header and main padding on
|
||||
// /odin): the chat owns the whole viewport, full-bleed.
|
||||
height: { xs: "100svh", md: "calc(100svh - 100px)" },
|
||||
display: "flex",
|
||||
border: 1,
|
||||
// Longhand on purpose: a responsive `border` shorthand lands in a
|
||||
// media query AFTER borderColor and resets the color to black.
|
||||
borderStyle: "solid",
|
||||
borderWidth: { xs: 0, md: 1 },
|
||||
borderColor: "divider",
|
||||
borderRadius: 3,
|
||||
borderRadius: { xs: 0, md: 3 },
|
||||
bgcolor: "background.paper",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
@@ -454,15 +471,41 @@ export default function OdinChat() {
|
||||
flexDirection: "column",
|
||||
gap: 1.5,
|
||||
p: 2,
|
||||
// Immersive mobile: respect the notch / home-indicator insets
|
||||
// (env() is 0 outside standalone mode, so max() keeps the 16px).
|
||||
pt: { xs: "max(16px, env(safe-area-inset-top))", md: 2 },
|
||||
pb: { xs: "max(16px, env(safe-area-inset-bottom))", md: 2 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
onClick={goBack}
|
||||
aria-label="Zpět"
|
||||
size="small"
|
||||
sx={{ ml: -0.5, flexShrink: 0 }}
|
||||
>
|
||||
<Box
|
||||
component="svg"
|
||||
viewBox="0 0 24 24"
|
||||
sx={{ width: 22, height: 22 }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="19" y1="12" x2="5" y2="12" />
|
||||
<polyline points="12 19 5 12 12 5" />
|
||||
</Box>
|
||||
</IconButton>
|
||||
)}
|
||||
{isMobile && (
|
||||
<IconButton
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
aria-label="Konverzace"
|
||||
size="small"
|
||||
sx={{ ml: -0.5, flexShrink: 0 }}
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
<Box
|
||||
component="svg"
|
||||
|
||||
Reference in New Issue
Block a user