| import React, { useState, useEffect } from 'react';
|
| import { motion, AnimatePresence } from 'framer-motion';
|
| import { FileText, Code, Eye, ArrowLeft, Download, Copy, Check, Maximize2, ZoomIn, ZoomOut, Search } from 'lucide-react';
|
| import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
| import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
| import { marked } from 'marked';
|
|
|
| const TextViewer = () => {
|
| const [text, setText] = useState('');
|
| const [fileName, setFileName] = useState('');
|
| const [viewMode, setViewMode] = useState('text');
|
| const [copied, setCopied] = useState(false);
|
| const [fontSize, setFontSize] = useState(16);
|
| const [searchTerm, setSearchTerm] = useState('');
|
| const [isSearching, setIsSearching] = useState(false);
|
|
|
| useEffect(() => {
|
|
|
| const urlParams = new URLSearchParams(window.location.search);
|
| const textParam = urlParams.get('text');
|
| const fileParam = urlParams.get('file');
|
|
|
| if (textParam) {
|
| setText(decodeURIComponent(textParam));
|
| } else {
|
| const savedText = localStorage.getItem('extractedText');
|
| if (savedText) setText(savedText);
|
| }
|
|
|
| if (fileParam) {
|
| setFileName(decodeURIComponent(fileParam));
|
| } else {
|
| setFileName(localStorage.getItem('fileName') || 'extracted-text');
|
| }
|
| }, []);
|
|
|
| const viewModes = [
|
| { id: 'text', label: 'Text', icon: <FileText size={18} />, ext: '.txt' },
|
| { id: 'markdown', label: 'Markdown', icon: <Code size={18} />, ext: '.md' },
|
| { id: 'html', label: 'Rendered', icon: <Eye size={18} />, ext: '.html' },
|
| { id: 'json', label: 'JSON', icon: <Code size={18} />, ext: '.json' }
|
| ];
|
|
|
| const handleCopy = async () => {
|
| try {
|
| await navigator.clipboard.writeText(text);
|
| setCopied(true);
|
| setTimeout(() => setCopied(false), 2000);
|
| } catch (err) {
|
| console.error('Failed to copy text:', err);
|
| }
|
| };
|
|
|
| const handleBack = () => {
|
| window.history.back();
|
| };
|
|
|
| const adjustFontSize = (delta) => {
|
| setFontSize(prev => Math.max(12, Math.min(24, prev + delta)));
|
| }; cons
|
| t renderContent = () => {
|
| const baseStyle = { fontSize: `${fontSize}px`, lineHeight: 1.7 };
|
|
|
| switch (viewMode) {
|
| case 'text':
|
| return (
|
| <div className="reader-content" style={baseStyle}>
|
| <pre className="text-reader">{text}</pre>
|
| </div>
|
| );
|
| case 'markdown':
|
| return (
|
| <div className="reader-content">
|
| <SyntaxHighlighter
|
| language="markdown"
|
| style={atomDark}
|
| customStyle={{
|
| ...baseStyle,
|
| background: 'transparent',
|
| border: 'none',
|
| padding: 0,
|
| margin: 0
|
| }}
|
| >
|
| {text}
|
| </SyntaxHighlighter>
|
| </div>
|
| );
|
| case 'html':
|
| return (
|
| <div
|
| className="reader-content html-reader"
|
| style={baseStyle}
|
| dangerouslySetInnerHTML={{ __html: marked(text) }}
|
| />
|
| );
|
| case 'json':
|
| const jsonData = {
|
| metadata: {
|
| fileName: fileName || 'extracted-text',
|
| extractedAt: new Date().toISOString(),
|
| characterCount: text.length,
|
| lineCount: text.split('\n').length,
|
| wordCount: text.split(/\s+/).filter(word => word.length > 0).length
|
| },
|
| content: {
|
| rawText: text,
|
| lines: text.split('\n'),
|
| paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
|
| }
|
| };
|
| return (
|
| <div className="reader-content">
|
| <SyntaxHighlighter
|
| language="json"
|
| style={atomDark}
|
| customStyle={{
|
| ...baseStyle,
|
| background: 'transparent',
|
| border: 'none',
|
| padding: 0,
|
| margin: 0
|
| }}
|
| >
|
| {JSON.stringify(jsonData, null, 2)}
|
| </SyntaxHighlighter>
|
| </div>
|
| );
|
| default:
|
| return null;
|
| }
|
| }; return
|
| (
|
| <div className="text-viewer-app">
|
| {/* Same background as main app */}
|
| <div className="viewer-background">
|
| <div className="giant-orb-background">
|
| <div className="giant-orb-container">
|
| <div className="gradient-orb orb-1"></div>
|
| <div className="gradient-orb orb-2"></div>
|
| <div className="gradient-orb orb-3"></div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div className="viewer-glass-container">
|
| {/* Professional Header */}
|
| <motion.header
|
| className="viewer-header"
|
| initial={{ opacity: 0, y: -20 }}
|
| animate={{ opacity: 1, y: 0 }}
|
| transition={{ duration: 0.6 }}
|
| >
|
| <div className="header-left">
|
| <motion.button
|
| className="back-button"
|
| onClick={handleBack}
|
| whileHover={{ scale: 1.05 }}
|
| whileTap={{ scale: 0.95 }}
|
| >
|
| <ArrowLeft size={18} />
|
| Back
|
| </motion.button>
|
|
|
| <div className="document-info">
|
| <h1 className="document-title">{fileName}</h1>
|
| <span className="document-stats">
|
| {text.length} chars • {text.split('\n').length} lines • {text.split(/\s+/).filter(w => w.length > 0).length} words
|
| </span>
|
| </div>
|
| </div>
|
|
|
| <div className="header-actions">
|
| <div className="search-container">
|
| <Search size={16} />
|
| <input
|
| type="text"
|
| placeholder="Search in document..."
|
| value={searchTerm}
|
| onChange={(e) => setSearchTerm(e.target.value)}
|
| className="search-input"
|
| />
|
| </div>
|
|
|
| <div className="font-controls">
|
| <button onClick={() => adjustFontSize(-2)} className="font-btn">
|
| <ZoomOut size={16} />
|
| </button>
|
| <span className="font-size">{fontSize}px</span>
|
| <button onClick={() => adjustFontSize(2)} className="font-btn">
|
| <ZoomIn size={16} />
|
| </button>
|
| </div>
|
|
|
| <motion.button
|
| className="action-btn"
|
| onClick={handleCopy}
|
| whileHover={{ scale: 1.05 }}
|
| whileTap={{ scale: 0.95 }}
|
| >
|
| <AnimatePresence mode="wait">
|
| {copied ? (
|
| <motion.div key="copied" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
|
| <Check size={16} />
|
| </motion.div>
|
| ) : (
|
| <motion.div key="copy" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
|
| <Copy size={16} />
|
| </motion.div>
|
| )}
|
| </AnimatePresence>
|
| {copied ? 'Copied!' : 'Copy'}
|
| </motion.button>
|
| </div>
|
| </motion.header>
|
| {/* Format Selector */}
|
| <motion.div
|
| className="format-selector"
|
| initial={{ opacity: 0, y: 20 }}
|
| animate={{ opacity: 1, y: 0 }}
|
| transition={{ duration: 0.6, delay: 0.1 }}
|
| >
|
| <div className="format-tabs">
|
| {viewModes.map((mode, index) => (
|
| <motion.button
|
| key={mode.id}
|
| className={`format-tab ${viewMode === mode.id ? 'active' : ''}`}
|
| onClick={() => setViewMode(mode.id)}
|
| whileHover={{ scale: 1.02 }}
|
| whileTap={{ scale: 0.98 }}
|
| initial={{ opacity: 0, x: -20 }}
|
| animate={{ opacity: 1, x: 0 }}
|
| transition={{ delay: index * 0.1 }}
|
| >
|
| {mode.icon}
|
| <span className="tab-label">{mode.label}</span>
|
| <span className="tab-ext">{mode.ext}</span>
|
|
|
| {viewMode === mode.id && (
|
| <motion.div
|
| className="tab-indicator"
|
| layoutId="viewModeIndicator"
|
| transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
| />
|
| )}
|
| </motion.button>
|
| ))}
|
| </div>
|
| </motion.div>
|
|
|
| {/* Professional Reader */}
|
| <motion.main
|
| className="reader-main"
|
| initial={{ opacity: 0, y: 20 }}
|
| animate={{ opacity: 1, y: 0 }}
|
| transition={{ duration: 0.6, delay: 0.2 }}
|
| >
|
| <div className="reader-panel">
|
| <AnimatePresence mode="wait">
|
| <motion.div
|
| key={viewMode}
|
| initial={{ opacity: 0, x: 20 }}
|
| animate={{ opacity: 1, x: 0 }}
|
| exit={{ opacity: 0, x: -20 }}
|
| transition={{ duration: 0.3 }}
|
| className="content-wrapper"
|
| >
|
| {renderContent()}
|
| </motion.div>
|
| </AnimatePresence>
|
| </div>
|
| </motion.main>
|
| </div>
|
| <style jsx>{`
|
| .text-viewer-app {
|
| min-height: 100vh;
|
| background: #0f0f23;
|
| color: #ffffff;
|
| font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
|
| position: relative;
|
| overflow-x: hidden;
|
| }
|
|
|
| .viewer-background {
|
| position: fixed;
|
| top: 0;
|
| left: 0;
|
| width: 100%;
|
| height: 100%;
|
| pointer-events: none;
|
| z-index: -1;
|
| }
|
|
|
| .giant-orb-background {
|
| position: fixed;
|
| top: 0;
|
| left: 0;
|
| width: 100%;
|
| height: 100%;
|
| pointer-events: none;
|
| z-index: -10;
|
| display: flex;
|
| align-items: center;
|
| justify-content: center;
|
| }
|
|
|
| .giant-orb-container {
|
| width: 100vmin;
|
| height: 100vmin;
|
| position: relative;
|
| pointer-events: none;
|
| transform: translateY(-5vh);
|
| }
|
|
|
| .gradient-orb {
|
| position: absolute;
|
| border-radius: 50%;
|
| filter: blur(100px);
|
| opacity: 0.3;
|
| }
|
|
|
| .orb-1 {
|
| top: 10%;
|
| right: 20%;
|
| width: 300px;
|
| height: 300px;
|
| background: linear-gradient(45deg, #4facfe, #8b5cf6);
|
| animation: float1 20s ease-in-out infinite;
|
| }
|
|
|
| .orb-2 {
|
| bottom: 20%;
|
| left: 15%;
|
| width: 250px;
|
| height: 250px;
|
| background: linear-gradient(45deg, #f093fb, #f5576c);
|
| animation: float2 25s ease-in-out infinite;
|
| }
|
|
|
| .orb-3 {
|
| top: 50%;
|
| left: 50%;
|
| width: 200px;
|
| height: 200px;
|
| background: linear-gradient(45deg, #06b6d4, #10b981);
|
| animation: float3 30s ease-in-out infinite;
|
| }
|
|
|
| @keyframes float1 {
|
| 0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
| 25% { transform: translate(-20px, -30px) rotate(90deg); }
|
| 50% { transform: translate(10px, -20px) rotate(180deg); }
|
| 75% { transform: translate(-10px, 10px) rotate(270deg); }
|
| }
|
|
|
| @keyframes float2 {
|
| 0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
| 33% { transform: translate(30px, -20px) rotate(120deg); }
|
| 66% { transform: translate(-20px, -40px) rotate(240deg); }
|
| }
|
|
|
| @keyframes float3 {
|
| 0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
|
| 20% { transform: translate(-65%, -35%) rotate(72deg); }
|
| 40% { transform: translate(-35%, -35%) rotate(144deg); }
|
| 60% { transform: translate(-35%, -65%) rotate(216deg); }
|
| 80% { transform: translate(-65%, -65%) rotate(288deg); }
|
| }
|
| .viewer-glass-container {
|
| backdrop-filter: blur(25px);
|
| -webkit-backdrop-filter: blur(25px);
|
| background: rgba(0, 0, 0, 0.1);
|
| border: none;
|
| border-radius: 16px;
|
| margin: 12px;
|
| min-height: calc(100vh - 24px);
|
| overflow: hidden;
|
| position: relative;
|
| z-index: 10;
|
| display: flex;
|
| flex-direction: column;
|
| }
|
|
|
| .viewer-header {
|
| display: flex;
|
| justify-content: space-between;
|
| align-items: center;
|
| padding: 20px 24px;
|
| border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| flex-shrink: 0;
|
| }
|
|
|
| .header-left {
|
| display: flex;
|
| align-items: center;
|
| gap: 20px;
|
| }
|
|
|
| .back-button {
|
| background: rgba(255, 255, 255, 0.05);
|
| border: 1px solid rgba(255, 255, 255, 0.1);
|
| border-radius: 8px;
|
| padding: 8px 16px;
|
| color: #ffffff;
|
| cursor: pointer;
|
| display: flex;
|
| align-items: center;
|
| gap: 6px;
|
| font-size: 0.85rem;
|
| transition: all 0.3s ease;
|
| backdrop-filter: blur(10px);
|
| }
|
|
|
| .back-button:hover {
|
| background: rgba(255, 255, 255, 0.08);
|
| border-color: rgba(79, 172, 254, 0.3);
|
| }
|
|
|
| .document-info {
|
| display: flex;
|
| flex-direction: column;
|
| gap: 4px;
|
| }
|
|
|
| .document-title {
|
| font-size: 1.25rem;
|
| font-weight: 600;
|
| color: #ffffff;
|
| margin: 0;
|
| }
|
|
|
| .document-stats {
|
| font-size: 0.75rem;
|
| color: rgba(255, 255, 255, 0.6);
|
| }
|
|
|
| .header-actions {
|
| display: flex;
|
| align-items: center;
|
| gap: 16px;
|
| }
|
|
|
| .search-container {
|
| display: flex;
|
| align-items: center;
|
| gap: 8px;
|
| background: rgba(255, 255, 255, 0.05);
|
| border: 1px solid rgba(255, 255, 255, 0.1);
|
| border-radius: 8px;
|
| padding: 6px 12px;
|
| backdrop-filter: blur(10px);
|
| }
|
|
|
| .search-input {
|
| background: none;
|
| border: none;
|
| color: #ffffff;
|
| font-size: 0.85rem;
|
| outline: none;
|
| width: 200px;
|
| }
|
|
|
| .search-input::placeholder {
|
| color: rgba(255, 255, 255, 0.5);
|
| }
|
|
|
| .font-controls {
|
| display: flex;
|
| align-items: center;
|
| gap: 8px;
|
| background: rgba(255, 255, 255, 0.05);
|
| border: 1px solid rgba(255, 255, 255, 0.1);
|
| border-radius: 8px;
|
| padding: 4px 8px;
|
| backdrop-filter: blur(10px);
|
| }
|
|
|
| .font-btn {
|
| background: none;
|
| border: none;
|
| color: rgba(255, 255, 255, 0.7);
|
| cursor: pointer;
|
| padding: 4px;
|
| border-radius: 4px;
|
| transition: all 0.3s ease;
|
| }
|
|
|
| .font-btn:hover {
|
| color: #ffffff;
|
| background: rgba(255, 255, 255, 0.1);
|
| }
|
|
|
| .font-size {
|
| font-size: 0.75rem;
|
| color: rgba(255, 255, 255, 0.8);
|
| min-width: 32px;
|
| text-align: center;
|
| }
|
|
|
| .action-btn {
|
| background: rgba(255, 255, 255, 0.05);
|
| border: 1px solid rgba(255, 255, 255, 0.1);
|
| border-radius: 8px;
|
| padding: 8px 16px;
|
| color: #ffffff;
|
| cursor: pointer;
|
| display: flex;
|
| align-items: center;
|
| gap: 6px;
|
| font-size: 0.85rem;
|
| transition: all 0.3s ease;
|
| backdrop-filter: blur(10px);
|
| }
|
|
|
| .action-btn:hover {
|
| background: rgba(255, 255, 255, 0.08);
|
| border-color: rgba(79, 172, 254, 0.3);
|
| }
|
| .format-selector {
|
| display: flex;
|
| justify-content: center;
|
| padding: 16px 24px;
|
| border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| flex-shrink: 0;
|
| }
|
|
|
| .format-tabs {
|
| display: flex;
|
| background: rgba(0, 0, 0, 0.3);
|
| border: 1px solid rgba(255, 255, 255, 0.1);
|
| border-radius: 12px;
|
| padding: 4px;
|
| backdrop-filter: blur(20px);
|
| }
|
|
|
| .format-tab {
|
| background: none;
|
| border: none;
|
| padding: 12px 20px;
|
| border-radius: 8px;
|
| cursor: pointer;
|
| display: flex;
|
| align-items: center;
|
| gap: 8px;
|
| font-size: 0.85rem;
|
| color: rgba(255, 255, 255, 0.7);
|
| transition: all 0.3s ease;
|
| position: relative;
|
| min-width: 120px;
|
| justify-content: center;
|
| }
|
|
|
| .format-tab:hover {
|
| color: #ffffff;
|
| background: rgba(255, 255, 255, 0.05);
|
| }
|
|
|
| .format-tab.active {
|
| color: #ffffff;
|
| background: rgba(255, 255, 255, 0.08);
|
| }
|
|
|
| .tab-label {
|
| font-weight: 500;
|
| }
|
|
|
| .tab-ext {
|
| font-size: 0.75rem;
|
| color: rgba(255, 255, 255, 0.5);
|
| font-weight: 400;
|
| }
|
|
|
| .tab-indicator {
|
| position: absolute;
|
| bottom: 0;
|
| left: 0;
|
| right: 0;
|
| height: 2px;
|
| background: linear-gradient(90deg, #4facfe, #8b5cf6);
|
| border-radius: 1px;
|
| }
|
|
|
| .reader-main {
|
| flex: 1;
|
| overflow: hidden;
|
| display: flex;
|
| flex-direction: column;
|
| }
|
|
|
| .reader-panel {
|
| flex: 1;
|
| overflow: auto;
|
| padding: 0;
|
| background: rgba(255, 255, 255, 0.02);
|
| margin: 16px 24px 24px;
|
| border-radius: 12px;
|
| backdrop-filter: blur(10px);
|
| }
|
|
|
| .content-wrapper {
|
| height: 100%;
|
| overflow: auto;
|
| }
|
|
|
| .reader-content {
|
| padding: 32px;
|
| max-width: 900px;
|
| margin: 0 auto;
|
| line-height: 1.8;
|
| }
|
|
|
| .text-reader {
|
| background: none;
|
| border: none;
|
| color: #ffffff;
|
| font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
| white-space: pre-wrap;
|
| word-wrap: break-word;
|
| width: 100%;
|
| margin: 0;
|
| padding: 0;
|
| line-height: inherit;
|
| } .ht
|
| ml-reader {
|
| color: #ffffff;
|
| font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
|
| }
|
|
|
| .html-reader h1,
|
| .html-reader h2,
|
| .html-reader h3,
|
| .html-reader h4,
|
| .html-reader h5,
|
| .html-reader h6 {
|
| color: #ffffff;
|
| margin-bottom: 24px;
|
| margin-top: 40px;
|
| font-weight: 600;
|
| line-height: 1.3;
|
| }
|
|
|
| .html-reader h1 {
|
| font-size: 2.5rem;
|
| background: linear-gradient(45deg, #4facfe, #8b5cf6);
|
| -webkit-background-clip: text;
|
| -webkit-text-fill-color: transparent;
|
| background-clip: text;
|
| border-bottom: 2px solid rgba(79, 172, 254, 0.3);
|
| padding-bottom: 16px;
|
| }
|
|
|
| .html-reader h2 {
|
| font-size: 2rem;
|
| color: #4facfe;
|
| border-left: 4px solid #4facfe;
|
| padding-left: 16px;
|
| }
|
|
|
| .html-reader h3 {
|
| font-size: 1.5rem;
|
| color: #8b5cf6;
|
| }
|
|
|
| .html-reader p {
|
| margin-bottom: 20px;
|
| color: rgba(255, 255, 255, 0.9);
|
| text-align: justify;
|
| }
|
|
|
| .html-reader strong,
|
| .html-reader b {
|
| color: #ffffff;
|
| font-weight: 700;
|
| text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
|
| }
|
|
|
| .html-reader em,
|
| .html-reader i {
|
| color: rgba(255, 255, 255, 0.95);
|
| font-style: italic;
|
| }
|
|
|
| .html-reader ul,
|
| .html-reader ol {
|
| margin-bottom: 20px;
|
| padding-left: 32px;
|
| }
|
|
|
| .html-reader li {
|
| margin-bottom: 12px;
|
| color: rgba(255, 255, 255, 0.9);
|
| position: relative;
|
| }
|
|
|
| .html-reader ul li::marker {
|
| color: #4facfe;
|
| }
|
|
|
| .html-reader ol li::marker {
|
| color: #8b5cf6;
|
| font-weight: 600;
|
| }
|
|
|
| .html-reader table {
|
| width: 100%;
|
| border-collapse: collapse;
|
| margin: 32px 0;
|
| background: rgba(0, 0, 0, 0.2);
|
| border-radius: 12px;
|
| overflow: hidden;
|
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
| }
|
|
|
| .html-reader th,
|
| .html-reader td {
|
| border: 1px solid rgba(255, 255, 255, 0.1);
|
| padding: 16px 20px;
|
| text-align: left;
|
| vertical-align: top;
|
| }
|
|
|
| .html-reader th {
|
| background: linear-gradient(135deg, rgba(79, 172, 254, 0.2), rgba(139, 92, 246, 0.2));
|
| font-weight: 700;
|
| color: #ffffff;
|
| text-transform: uppercase;
|
| font-size: 0.85rem;
|
| letter-spacing: 0.5px;
|
| }
|
|
|
| .html-reader td {
|
| background: rgba(255, 255, 255, 0.02);
|
| }
|
|
|
| .html-reader tr:nth-child(even) td {
|
| background: rgba(255, 255, 255, 0.04);
|
| }
|
|
|
| .html-reader tr:hover td {
|
| background: rgba(79, 172, 254, 0.05);
|
| }
|
|
|
| .html-reader blockquote {
|
| border-left: 4px solid #4facfe;
|
| padding: 20px 24px;
|
| margin: 24px 0;
|
| background: rgba(79, 172, 254, 0.05);
|
| border-radius: 0 8px 8px 0;
|
| font-style: italic;
|
| color: rgba(255, 255, 255, 0.9);
|
| position: relative;
|
| }
|
|
|
| .html-reader blockquote::before {
|
| content: '"';
|
| font-size: 4rem;
|
| color: rgba(79, 172, 254, 0.3);
|
| position: absolute;
|
| top: -10px;
|
| left: 10px;
|
| font-family: serif;
|
| }
|
|
|
| .html-reader code {
|
| background: rgba(0, 0, 0, 0.4);
|
| padding: 4px 8px;
|
| border-radius: 6px;
|
| font-family: 'SF Mono', Monaco, monospace;
|
| font-size: 0.9em;
|
| color: #4facfe;
|
| border: 1px solid rgba(79, 172, 254, 0.2);
|
| }
|
|
|
| .html-reader pre {
|
| background: rgba(0, 0, 0, 0.4);
|
| padding: 24px;
|
| border-radius: 12px;
|
| overflow-x: auto;
|
| margin: 24px 0;
|
| border: 1px solid rgba(255, 255, 255, 0.1);
|
| position: relative;
|
| }
|
|
|
| .html-reader pre code {
|
| background: none;
|
| padding: 0;
|
| border: none;
|
| color: rgba(255, 255, 255, 0.9);
|
| }
|
|
|
| .html-reader a {
|
| color: #4facfe;
|
| text-decoration: none;
|
| border-bottom: 1px solid rgba(79, 172, 254, 0.3);
|
| transition: all 0.3s ease;
|
| }
|
|
|
| .html-reader a:hover {
|
| color: #8b5cf6;
|
| border-bottom-color: rgba(139, 92, 246, 0.5);
|
| text-shadow: 0 0 8px rgba(139, 92, 246, 0.3);
|
| }
|
|
|
| .html-reader hr {
|
| border: none;
|
| height: 2px;
|
| background: linear-gradient(90deg, transparent, rgba(79, 172, 254, 0.5), transparent);
|
| margin: 40px 0;
|
| } |