From 28fb3992170604a525ab2df8e4b1eba0f1307d81 Mon Sep 17 00:00:00 2001 From: BOHA Date: Mon, 8 Jun 2026 21:03:16 +0200 Subject: [PATCH] =?UTF-8?q?feat(odin):=20refine=20hero=20=E2=80=94=20Newsr?= =?UTF-8?q?eader=20serif,=20red=20name,=20staggered=20entrance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Swap Fraunces → Newsreader (literary serif, full Czech, optical sizing). - Split the greeting so the user's first name renders in brand-red italic. - Add a subtle staggered fade-in (framer-motion), skipped under prefers-reduced-motion. - New description that names Odin and covers chat + invoice import. Co-Authored-By: Claude Opus 4.8 (1M context) --- index.html | 2 +- src/admin/components/odin/OdinChat.tsx | 30 +++--- src/admin/components/odin/OdinThread.tsx | 116 ++++++++++++++++------- 3 files changed, 96 insertions(+), 52 deletions(-) diff --git a/index.html b/index.html index 6ecf1b1..53ad3dc 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@ diff --git a/src/admin/components/odin/OdinChat.tsx b/src/admin/components/odin/OdinChat.tsx index 82ed623..e37ee18 100644 --- a/src/admin/components/odin/OdinChat.tsx +++ b/src/admin/components/odin/OdinChat.tsx @@ -67,21 +67,18 @@ export default function OdinChat() { // Odin itself only needs ai.use, so the Save affordance is gated separately. const canSave = hasPermission("invoices.create"); // Time-based Czech greeting + the user's first name (client-local time). - const greeting = (() => { - const h = new Date().getHours(); - const part = - h < 9 - ? "Dobré ráno" - : h < 12 - ? "Dobrý den" - : h < 18 - ? "Dobré odpoledne" - : "Dobrý večer"; - const name = (user?.fullName || user?.username || "") - .trim() - .split(/\s+/)[0]; - return name ? `${part}, ${name}` : part; - })(); + const greetingHour = new Date().getHours(); + const greetingPhrase = + greetingHour < 9 + ? "Dobré ráno" + : greetingHour < 12 + ? "Dobrý den" + : greetingHour < 18 + ? "Dobré odpoledne" + : "Dobrý večer"; + const userName = (user?.fullName || user?.username || "") + .trim() + .split(/\s+/)[0]; const { data: usage } = useQuery(aiUsageOptions()); const { data: convData, isPending: convPending } = useQuery( aiConversationsOptions(), @@ -441,7 +438,8 @@ export default function OdinChat() { turns={turns} busy={busy} loading={messagesLoading} - greeting={greeting} + greetingPhrase={greetingPhrase} + userName={userName} threadRef={threadRef} /> diff --git a/src/admin/components/odin/OdinThread.tsx b/src/admin/components/odin/OdinThread.tsx index ef821e5..9a85646 100644 --- a/src/admin/components/odin/OdinThread.tsx +++ b/src/admin/components/odin/OdinThread.tsx @@ -1,13 +1,31 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; +import { motion, useReducedMotion, type Variants } from "framer-motion"; import type { ChatTurn } from "./types"; import OdinMark from "./OdinMark"; +const MotionBox = motion.create(Box); + +// Staggered hero entrance (skipped under prefers-reduced-motion). +const heroContainer: Variants = { + hidden: {}, + show: { transition: { staggerChildren: 0.12, delayChildren: 0.06 } }, +}; +const heroItem: Variants = { + hidden: { opacity: 0, y: 14 }, + show: { + opacity: 1, + y: 0, + transition: { duration: 0.55, ease: [0.16, 1, 0.3, 1] }, + }, +}; + interface OdinThreadProps { turns: ChatTurn[]; busy: boolean; loading: boolean; - greeting?: string; + greetingPhrase?: string; + userName?: string; threadRef: React.RefObject; } @@ -38,9 +56,15 @@ export default function OdinThread({ turns, busy, loading, - greeting, + greetingPhrase, + userName, threadRef, }: OdinThreadProps) { + const reduce = useReducedMotion(); + const containerMotion = reduce + ? {} + : { variants: heroContainer, initial: "hidden", animate: "show" }; + const itemMotion = reduce ? {} : { variants: heroItem }; return ( - - {greeting || "Dobrý den"} - - - Váš firemní AI asistent — zeptejte se na cokoli, nebo přiložte - fakturu (PDF) k importu. - - + + + {greetingPhrase || "Dobrý den"} + {userName && ( + <> + ,{" "} + ({ + fontStyle: "italic", + backgroundImage: `linear-gradient(120deg, ${t.vars!.palette.primary.light}, ${t.vars!.palette.primary.main})`, + WebkitBackgroundClip: "text", + backgroundClip: "text", + WebkitTextFillColor: "transparent", + })} + > + {userName} + + + )} + + + + + Zeptejte se na cokoli, nebo přiložte fakturu — Odin ji přečte a + připraví k uložení. + + + )} {/* Message bubbles */}