fix dossier and shadows for scrollable dialogues

Этот коммит содержится в:
Constantin Rusu 2025-01-28 00:45:38 +00:00
родитель a457733ce9
Коммит 444400bddf
6 изменённых файлов: 166 добавлений и 80 удалений

Просмотреть файл

@ -54,8 +54,8 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
{t('dossier.button')} {t('dossier.button')}
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent className="w-[95vw] sm:w-[90vw] lg:w-[45vw] bg-[#1a1a1a] border-gray-700 text-white overflow-hidden p-8 pt-10 !max-w-[100vw]"> <SheetContent className="w-[95vw] sm:w-[90vw] lg:w-[45vw] bg-[#1a1a1a] border-gray-700 text-white overflow-hidden p-2 sm:p-8 pt-10 !max-w-[100vw] flex flex-col">
<SheetHeader className="mb-6"> <SheetHeader className="mb-6 flex-none">
<SheetTitle className="text-yellow-500 relative"> <SheetTitle className="text-yellow-500 relative">
<span className="absolute -top-6 left-0 text-xs text-red-500 tracking-wider font-mono"> <span className="absolute -top-6 left-0 text-xs text-red-500 tracking-wider font-mono">
{t('dossier.clearanceRequired')} {t('dossier.clearanceRequired')}
@ -64,13 +64,14 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
</SheetTitle> </SheetTitle>
</SheetHeader> </SheetHeader>
<div className="bg-gray-800/30 p-6 rounded-md border border-gray-700 mb-6"> <ScrollArea className="flex-1 min-h-0">
<MetricsDisplay choices={choices} className="pl-0" /> <div className="space-y-6 pb-4 px-2 sm:px-4">
</div> <div className="bg-gray-800/30 p-4 sm:p-6 rounded-md border border-gray-700">
<MetricsDisplay choices={choices} className="pl-0" />
</div>
<Separator className="my-6 bg-gray-700" /> <Separator className="bg-gray-700" />
<ScrollArea className="h-[calc(100vh-320px)] pr-4">
<div className="space-y-6 pb-16">
{entries.length === 0 ? ( {entries.length === 0 ? (
<p className="text-gray-400 italic">{t('dossier.noIntelligence')}</p> <p className="text-gray-400 italic">{t('dossier.noIntelligence')}</p>
) : ( ) : (
@ -80,7 +81,7 @@ export const DossierPanel = ({ entries, choices = [] }: DossierPanelProps) => {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }} transition={{ duration: 0.5, delay: index * 0.1 }}
className="space-y-4 relative bg-gray-800/30 p-6 rounded-md border border-gray-700" className="space-y-4 relative bg-gray-800/30 p-4 sm:p-6 rounded-md border border-gray-700"
> >
<div> <div>
<h3 className="text-yellow-500 font-semibold flex items-center gap-3"> <h3 className="text-yellow-500 font-semibold flex items-center gap-3">

Просмотреть файл

@ -66,19 +66,21 @@ export const EndGameDialog = ({ onContinue, startFade }: EndGameDialogProps) =>
return ( return (
<Dialog open={open}> <Dialog open={open}>
<DialogPortal> <DialogPortal>
<DialogOverlay className="z-[45]" /> <DialogOverlay className={cn(
<DialogContent "bg-black/95 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className={cn( startFade && "animate-fade-out"
"bg-black/95 text-white border-emerald-900/50 max-w-2xl [&>button]:hidden", )} />
"z-[50] fixed left-[50%] top-[50%] grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg" <DialogContent className={cn(
)} "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-emerald-900/50 bg-black/95 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2 sm:rounded-lg",
onPointerDownOutside={(e) => e.preventDefault()} startFade && "animate-fade-out",
onEscapeKeyDown={(e) => e.preventDefault()} "max-h-[90vh] overflow-y-auto scrollbar-thin scrollbar-thumb-emerald-500/20 scrollbar-track-transparent",
> "relative after:absolute after:bottom-0 after:left-0 after:right-0 after:h-16 after:bg-gradient-to-t after:from-black/95 after:to-transparent after:pointer-events-none after:z-50"
)}>
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.95 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }} transition={{ duration: 0.8, ease: "easeOut" }}
className="relative z-10 pb-16"
> >
<DialogHeader> <DialogHeader>
<DialogTitle className="text-emerald-500 text-2xl mb-6"> <DialogTitle className="text-emerald-500 text-2xl mb-6">

Просмотреть файл

@ -21,9 +21,12 @@
inset 0 0 60px rgba(0, 0, 0, 0.6); inset 0 0 60px rgba(0, 0, 0, 0.6);
width: 100%; width: 100%;
max-width: 800px; max-width: 800px;
max-height: 80vh;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
color: #e8e8e8; color: #e8e8e8;
display: flex;
flex-direction: column;
} }
@media (min-width: 640px) { @media (min-width: 640px) {
@ -92,12 +95,56 @@
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.5; line-height: 1.5;
text-shadow: 0 0 1px rgba(255, 255, 255, 0.1); text-shadow: 0 0 1px rgba(255, 255, 255, 0.1);
overflow-y: auto;
flex: 1;
padding-right: 0.5rem;
position: relative;
-webkit-overflow-scrolling: touch;
max-height: calc(80vh - 12rem);
} }
/* Gradient container */
.memo-gradient {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background: linear-gradient(to top, #1a1715 10%, rgba(26, 23, 21, 0.8) 40%, transparent 100%);
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 10;
}
.memo-gradient.show {
opacity: 0.95;
}
/* Custom scrollbar for WebKit browsers */
.memo-body::-webkit-scrollbar {
width: 8px;
}
.memo-body::-webkit-scrollbar-track {
background: transparent;
}
.memo-body::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
/* Show scrollbar on hover */
.memo-body:hover::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
}
/* Remove the old gradient and padding styles */
.memo-body p { .memo-body p {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.memo-body p:last-child { .memo-body p:last-child {
margin-bottom: 0; margin-bottom: 1rem;
} }

Просмотреть файл

@ -1,7 +1,8 @@
import React from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import './ExpertMemo.css'; import './ExpertMemo.css';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { BriefingAudio } from './BriefingAudio'; import { BriefingAudio } from './BriefingAudio';
import { cn } from '@/lib/utils';
interface ExpertMemoProps { interface ExpertMemoProps {
from: string; from: string;
@ -16,6 +17,31 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({ from, subject, children,
const { t } = useTranslation(); const { t } = useTranslation();
const highlightColor = isAlert ? 'text-red-500' : 'text-yellow-500'; const highlightColor = isAlert ? 'text-red-500' : 'text-yellow-500';
const memoClass = isAlert ? 'expert-memo alert' : 'expert-memo'; const memoClass = isAlert ? 'expert-memo alert' : 'expert-memo';
const [showGradient, setShowGradient] = useState(false);
const memoBodyRef = useRef<HTMLDivElement>(null);
const checkScroll = useCallback(() => {
const element = memoBodyRef.current;
if (element) {
const hasOverflow = element.scrollHeight > element.clientHeight;
const isAtBottom = Math.abs(element.scrollHeight - element.clientHeight - element.scrollTop) < 1;
setShowGradient(hasOverflow && !isAtBottom);
}
}, []);
useEffect(() => {
const element = memoBodyRef.current;
if (element) {
checkScroll();
element.addEventListener('scroll', checkScroll);
window.addEventListener('resize', checkScroll);
return () => {
element.removeEventListener('scroll', checkScroll);
window.removeEventListener('resize', checkScroll);
};
}
}, [checkScroll]);
// Function to wrap text content in paragraph tags // Function to wrap text content in paragraph tags
const formatContent = (content: React.ReactNode) => { const formatContent = (content: React.ReactNode) => {
@ -53,9 +79,10 @@ export const ExpertMemo: React.FC<ExpertMemoProps> = ({ from, subject, children,
</div> </div>
)} )}
</div> </div>
<div className="memo-body text-gray-300"> <div ref={memoBodyRef} className="memo-body">
{formatContent(children)} {formatContent(children)}
</div> </div>
<div className={cn("memo-gradient", showGradient && "show")} />
</div> </div>
); );
}; };

Просмотреть файл

@ -24,48 +24,54 @@ export const IntroDialog = ({ onStartAudio }: IntroDialogProps) => {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="bg-black/90 text-white border-gray-700 max-w-2xl"> <DialogContent
<DialogHeader> className="bg-black text-white border-gray-700 max-w-2xl max-h-[85vh] overflow-y-auto"
<DialogTitle className="text-yellow-500 text-2xl mb-6"> >
{t('intro.title')} <div className="relative flex-1 min-h-0">
</DialogTitle> <div className="space-y-6">
<DialogHeader>
<div className="space-y-6 text-gray-200"> <DialogTitle className="text-yellow-500 text-2xl mb-6">
<div className="flex items-center gap-4"> {t('intro.title')}
<div className="text-4xl">🎯</div> </DialogTitle>
<p className="text-lg font-medium">
{t('intro.mission')} <div className="space-y-6 text-gray-200">
</p> <div className="flex items-center gap-4">
</div> <div className="text-4xl">🎯</div>
<p className="text-lg font-medium">
<p className="text-base"> {t('intro.mission')}
{t('intro.explanation')} </p>
</p> </div>
<p className="text-base"> <p className="text-base">
{t('intro.howToPlay.description')} {t('intro.explanation')}
</p> </p>
<p className="text-yellow-500 text-sm"> <p className="text-base">
{t('intro.reminder')} {t('intro.howToPlay.description')}
</p> </p>
</div>
</DialogHeader> <p className="text-yellow-500 text-sm">
{t('intro.reminder')}
</p>
</div>
</DialogHeader>
<div className="flex flex-col items-center gap-6 mt-8"> <div className="flex flex-col items-center gap-6 mt-8">
<div className="flex items-center gap-2 self-start"> <div className="flex items-center gap-2 self-start">
<LanguageSwitcher /> <LanguageSwitcher />
<span className="text-xs text-gray-400"> <span className="text-xs text-gray-400">
{t('languageSwitcher.hint')} {t('languageSwitcher.hint')}
</span> </span>
</div>
<Button
onClick={handleBeginSimulation}
className="bg-yellow-500 hover:bg-yellow-600 text-black font-semibold w-full py-6 text-lg"
>
{t('buttons.beginSimulation')}
</Button>
</div>
</div> </div>
<Button
onClick={handleBeginSimulation}
className="bg-yellow-500 hover:bg-yellow-600 text-black font-semibold w-full py-6 text-lg"
>
{t('buttons.beginSimulation')}
</Button>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

Просмотреть файл

@ -32,25 +32,28 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => {
<DialogPortal> return (
<DialogOverlay /> <DialogPortal>
<DialogPrimitive.Content <DialogOverlay />
ref={ref} <DialogPrimitive.Content
className={cn( ref={ref}
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", className={cn(
className "fixed left-[50%] top-[50%] z-50 flex flex-col w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2 sm:rounded-lg",
)} "max-h-[85vh] overflow-y-auto scrollbar-thin scrollbar-thumb-gray-400 scrollbar-track-transparent",
{...props} className
> )}
{children} {...props}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> >
<X className="h-4 w-4" /> {children}
<span className="sr-only">Close</span> <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
</DialogPrimitive.Close> <X className="h-4 w-4" />
</DialogPrimitive.Content> <span className="sr-only">Close</span>
</DialogPortal> </DialogPrimitive.Close>
)) </DialogPrimitive.Content>
</DialogPortal>
);
})
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({ const DialogHeader = ({