|
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;
|
|
} |