1import { useEffect, useState } from 'react'; 2import { html } from '../html.js?v=3.8.36'; 3import { Archive, ArrowRight, X } from 'lucide-react'; 4import { 5 readTourState, 6 recordTourOutcome, 7 shouldShowTourEntry, 8 TOUR_OUTCOMES, 9} from '../services/tourState.js?v=3.8.36'; 10import { getPrivacyDetailsHref } from '../services/privacyRoute.js?v=3.8.36'; 11 12const getStorage = () => { 13 try { 14 return window.localStorage; 15 } catch { 16 return null; 17 } 18}; 19 20// Kept under its established component name, but rendered as a non-blocking 21// invitation so a first visit can always use the archive immediately. 22const WelcomeModal = ({ onStart }) => { 23 const [isVisible, setIsVisible] = useState(false); 24 25 useEffect(() => { 26 setIsVisible(shouldShowTourEntry(readTourState(getStorage()))); 27 }, []); 28 29 const finishEntry = (outcome) => { 30 recordTourOutcome(getStorage(), outcome); 31 // Always hide for the current session, even when storage is unavailable. 32 setIsVisible(false); 33 }; 34 35 const handleDismiss = () => { 36 finishEntry(TOUR_OUTCOMES.dismissed); 37 }; 38 39 const handleStart = () => { 40 finishEntry(TOUR_OUTCOMES.completed); 41 onStart?.(); 42 }; 43 44 const handlePrivacy = () => { 45 finishEntry(TOUR_OUTCOMES.dismissed); 46 }; 47 48 if (!isVisible || !onStart) return null; 49 50 return html` 51 <aside 52 className="archive-panel archive-panel--accent archive-welcome-panel fixed z-40 bottom-4 right-4 w-full max-w-[calc(100vw-2rem)] sm:max-w-md" 53 aria-labelledby="welcome-title" 54 aria-describedby="welcome-description" 55 > 56 <div className="archive-welcome-panel__privacy"> 57 <span><strong>Privacy:</strong> No tracking cookies; local preferences.</span> 58 <a href=${getPrivacyDetailsHref()} onClick=${handlePrivacy}>Details</a> 59 </div> 60 61 <div className="archive-welcome-panel__body"> 62 <button 63 onClick=${handleDismiss} 64 className="archive-action archive-action--quiet archive-welcome-panel__close absolute top-2 right-2 p-3" 65 aria-label="Dismiss Start here invitation" 66 > 67 <${X} className="w-5 h-5" aria-hidden="true" /> 68 </button> 69 70 <div className="flex items-center gap-3 pr-8"> 71 <div className="bg-stone-900 text-white p-2 flex-shrink-0" aria-hidden="true"> 72 <${Archive} className="w-5 h-5" /> 73 </div> 74 <h2 id="welcome-title" className="text-xl font-display font-bold text-stone-900"> 75 Find your way in 76 </h2> 77 </div> 78 <p id="welcome-description" className="text-sm text-stone-600 leading-relaxed font-body mt-3"> 79 Begin with a landmark work, search the collection, or follow an idea across four decades. 80 </p> 81 <div className="flex flex-wrap items-center gap-2 mt-4"> 82 <button 83 onClick=${handleStart} 84 className="archive-action archive-action--primary group py-3" 85 > 86 <span>Start here</span> 87 <${ArrowRight} className="w-4 h-4 transition-transform group-hover:translate-x-1" aria-hidden="true" /> 88 </button> 89 <button 90 onClick=${handleDismiss} 91 className="archive-action archive-action--quiet py-3" 92 > 93 Maybe later 94 </button> 95 </div> 96 </div> 97 </aside> 98 `; 99}; 100 101export default WelcomeModal;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.