ui: AMOLED theme across all 30 tools, blue accent, squared edges
This commit is contained in:
parent
634a2cddcd
commit
35d84857e3
35 changed files with 1645 additions and 1649 deletions
|
|
@ -3,7 +3,9 @@
|
|||
:root {
|
||||
--background: #000000;
|
||||
--foreground: #e5e5e5;
|
||||
--accent: #10b981;
|
||||
--accent: #60a5fa;
|
||||
--accent-hover: #3b82f6;
|
||||
--accent-dim: #1e3a5f;
|
||||
--surface: #0a0a0a;
|
||||
--surface-hover: #111111;
|
||||
--border: #1a1a1a;
|
||||
|
|
@ -26,12 +28,6 @@ body {
|
|||
}
|
||||
|
||||
/* Dot pattern background */
|
||||
.dot-bg {
|
||||
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.035) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
/* Fade the dots at top and bottom */
|
||||
.dot-bg-fade {
|
||||
position: relative;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function Home() {
|
|||
{Object.entries(toolsByCategory).map(([category, categoryTools]) => (
|
||||
<section key={category} className="mb-8">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="h-px w-4 bg-emerald-900"></div>
|
||||
<div className="h-px w-4 bg-blue-900"></div>
|
||||
<h2 className="text-xs font-medium uppercase tracking-widest text-[#525252]">
|
||||
{category}
|
||||
</h2>
|
||||
|
|
|
|||
|
|
@ -161,12 +161,12 @@ export default function Base64Page() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Controls Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-[#1a1a1a]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEncode}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
|
|
@ -176,7 +176,7 @@ export default function Base64Page() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleDecode}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
|
|
@ -187,7 +187,7 @@ export default function Base64Page() {
|
|||
type="button"
|
||||
onClick={handleSwap}
|
||||
disabled={!output && !input}
|
||||
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-200 rounded-lg text-sm font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-2 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-sm font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
title="Swap input and output"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -199,7 +199,7 @@ export default function Base64Page() {
|
|||
type="button"
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-950/30 text-gray-700 dark:text-gray-300 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed rounded-lg text-sm font-medium transition-colors cursor-pointer"
|
||||
className="px-3 py-2 bg-[#111] hover:bg-[#1a0a0a] text-gray-400 hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed text-sm font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -207,25 +207,25 @@ export default function Base64Page() {
|
|||
|
||||
{/* Sample buttons */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400 font-medium">Samples:</span>
|
||||
<span className="text-gray-500 font-medium">Samples:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("text")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Plain Text
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("unicode")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Unicode / Emojis
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("base64")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Base64
|
||||
</button>
|
||||
|
|
@ -233,7 +233,7 @@ export default function Base64Page() {
|
|||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-400">
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -245,9 +245,9 @@ export default function Base64Page() {
|
|||
encodeBase64(input, val, splitLines);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500 dark:bg-gray-700"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>URL-safe Base64 <code className="text-xs bg-gray-100 dark:bg-gray-800 px-1 py-0.5 rounded">(-_ instead of +/)</code></span>
|
||||
<span>URL-safe Base64 <code className="text-xs bg-[#111] px-1 py-0.5 rounded">(-_ instead of +/)</code></span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
|
|
@ -260,7 +260,7 @@ export default function Base64Page() {
|
|||
encodeBase64(input, urlSafe, val);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500 dark:bg-gray-700"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Wrap lines at 76 characters (MIME)</span>
|
||||
</label>
|
||||
|
|
@ -268,7 +268,7 @@ export default function Base64Page() {
|
|||
|
||||
{/* Error Notification */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-800 rounded-lg text-red-800 dark:text-red-300 text-sm">
|
||||
<div className="flex items-start gap-3 p-4 bg-[#1a0a0a] border border-red-900 text-red-400 text-sm">
|
||||
<svg className="w-5 h-5 text-red-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
|
@ -284,10 +284,10 @@ export default function Base64Page() {
|
|||
{/* Input Area */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={inputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={inputId} className="font-medium text-sm text-gray-400">
|
||||
Input String
|
||||
</label>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{input.length} chars | {inputBytes} bytes
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -300,7 +300,7 @@ export default function Base64Page() {
|
|||
}}
|
||||
placeholder="Enter or paste text to encode, or Base64 string to decode..."
|
||||
rows={12}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed shadow-inner"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y placeholder:text-gray-400 leading-relaxed shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -308,31 +308,31 @@ export default function Base64Page() {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor={outputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={outputId} className="font-medium text-sm text-gray-400">
|
||||
Output Result
|
||||
</label>
|
||||
{lastAction && (
|
||||
<span className="px-2 py-0.5 text-xs rounded-full font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300">
|
||||
<span className="px-2 py-0.5 text-xs rounded-none font-medium bg-[#0a0a1a] text-blue-400">
|
||||
{lastAction === "encode" ? "Base64 Encoded" : "Decoded Text"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{output.length} chars | {outputBytes} bytes
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
disabled={!output}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-green-600 dark:text-green-400 font-semibold">Copied!</span>
|
||||
<span className="text-green-400 font-semibold">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -351,23 +351,23 @@ export default function Base64Page() {
|
|||
readOnly
|
||||
placeholder="Encoded or decoded output will appear here..."
|
||||
rows={12}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/60 text-gray-900 dark:text-gray-100 font-mono text-sm focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed select-all"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-black text-gray-200 font-mono text-sm focus:outline-none resize-y placeholder:text-gray-400 leading-relaxed select-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Information & Feature Highlights */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800 grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-800/40 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">Full UTF-8 Unicode Support</h4>
|
||||
<div className="mt-8 pt-6 border-t border-[#1a1a1a] grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-gray-600">
|
||||
<div className="p-3 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">Full UTF-8 Unicode Support</h4>
|
||||
<p>Handles international character sets, special symbols, and emojis accurately without Latin1 byte truncation errors.</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-800/40 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">100% Client-Side & Private</h4>
|
||||
<div className="p-3 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">100% Client-Side & Private</h4>
|
||||
<p>Your data never leaves your browser. All encoding and decoding operations execute locally in JavaScript.</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-800/40 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">URL-Safe & MIME Formatting</h4>
|
||||
<div className="p-3 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">URL-Safe & MIME Formatting</h4>
|
||||
<p>Convert standard Base64 characters into URL-friendly variants or wrap output lines for MIME/email standards.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -218,21 +218,21 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Live Preview Area */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Live Preview
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 hidden sm:inline">
|
||||
<span className="text-xs font-medium text-gray-500 hidden sm:inline">
|
||||
Background:
|
||||
</span>
|
||||
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 p-0.5 rounded-lg text-xs">
|
||||
<div className="flex items-center gap-1 bg-[#111] p-0.5 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewBg("light")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
previewBg === "light"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Light
|
||||
|
|
@ -240,10 +240,10 @@ export default function BoxShadowGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewBg("dark")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
previewBg === "dark"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Dark
|
||||
|
|
@ -251,10 +251,10 @@ export default function BoxShadowGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewBg("checker")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
previewBg === "checker"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Pattern
|
||||
|
|
@ -264,12 +264,12 @@ export default function BoxShadowGeneratorPage() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
className={`relative w-full h-72 sm:h-80 rounded-2xl border border-gray-200 dark:border-gray-700 flex items-center justify-center p-6 overflow-hidden transition-all ${
|
||||
className={`relative w-full h-72 sm:h-80 border border-[#1a1a1a] flex items-center justify-center p-6 overflow-hidden transition-all ${
|
||||
previewBg === "light"
|
||||
? "bg-gray-100"
|
||||
? "bg-[#111]"
|
||||
: previewBg === "dark"
|
||||
? "bg-gray-950"
|
||||
: "bg-[radial-gradient(#cbd5e1_1px,transparent_1px)] dark:bg-[radial-gradient(#334155_1px,transparent_1px)] [background-size:16px_16px] bg-gray-100 dark:bg-gray-900"
|
||||
: "bg-[radial-gradient(#cbd5e1_1px,transparent_1px)](#334155_1px,transparent_1px)] [background-size:16px_16px] bg-[#111]"
|
||||
}`}
|
||||
>
|
||||
{/* The Shadow Card */}
|
||||
|
|
@ -281,7 +281,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
boxShadow: shadowValue,
|
||||
}}
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-blue-50 dark:bg-blue-900/30 flex items-center justify-center text-blue-600 dark:text-blue-400 mb-2">
|
||||
<div className="w-10 h-10 rounded-none bg-[#0a0a1a] flex items-center justify-center text-blue-600 mb-2">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
|
|
@ -291,7 +291,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-800">
|
||||
<h3 className="text-sm font-semibold text-gray-200">
|
||||
Shadow Preview
|
||||
</h3>
|
||||
<p className="text-[11px] font-mono text-gray-500 mt-1">
|
||||
|
|
@ -304,13 +304,13 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Presets Gallery */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Quick Presets
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetDefaults}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline font-medium cursor-pointer"
|
||||
className="text-xs text-blue-600 hover:underline font-medium cursor-pointer"
|
||||
>
|
||||
Reset to Default
|
||||
</button>
|
||||
|
|
@ -326,13 +326,13 @@ export default function BoxShadowGeneratorPage() {
|
|||
key={preset.name}
|
||||
type="button"
|
||||
onClick={() => applyPreset(preset)}
|
||||
className="group flex flex-col items-center justify-center p-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-blue-500 dark:hover:border-blue-500 transition-all hover:scale-[1.03]"
|
||||
className="group flex flex-col items-center justify-center p-3 border border-[#1a1a1a] bg-[#0a0a0a] hover:border-blue-500 transition-all hover:scale-[1.03]"
|
||||
>
|
||||
<div
|
||||
className="w-10 h-8 rounded-md bg-white border border-gray-100 transition-all mb-2"
|
||||
className="w-10 h-8 bg-[#0a0a0a] border border-gray-100 transition-all mb-2"
|
||||
style={{ boxShadow: pShadow }}
|
||||
/>
|
||||
<span className="text-[11px] font-medium text-gray-700 dark:text-gray-300 text-center w-full truncate">
|
||||
<span className="text-[11px] font-medium text-gray-400 text-center w-full truncate">
|
||||
{preset.name}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -344,8 +344,8 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Shadow Control Sliders Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Controls Column 1: Dimensions & Geometry */}
|
||||
<div className="space-y-5 p-5 rounded-xl border border-gray-200 dark:border-gray-700/80 bg-gray-50/50 dark:bg-gray-800/50">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<div className="space-y-5 p-5 border border-[#1a1a1a] bg-black/50">
|
||||
<h3 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16m-7 6h7" />
|
||||
</svg>
|
||||
|
|
@ -355,7 +355,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Horizontal Offset */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Horizontal Offset (X)
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -365,9 +365,9 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="50"
|
||||
value={horizontal}
|
||||
onChange={(e) => setHorizontal(Number(e.target.value))}
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">px</span>
|
||||
<span className="text-xs text-gray-500 font-mono">px</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -376,7 +376,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="50"
|
||||
value={horizontal}
|
||||
onChange={(e) => setHorizontal(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-gray-400 font-mono">
|
||||
<span>-50px</span>
|
||||
|
|
@ -388,7 +388,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Vertical Offset */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Vertical Offset (Y)
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -398,9 +398,9 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="50"
|
||||
value={vertical}
|
||||
onChange={(e) => setVertical(Number(e.target.value))}
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">px</span>
|
||||
<span className="text-xs text-gray-500 font-mono">px</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -409,7 +409,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="50"
|
||||
value={vertical}
|
||||
onChange={(e) => setVertical(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-gray-400 font-mono">
|
||||
<span>-50px</span>
|
||||
|
|
@ -421,7 +421,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Blur Radius */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Blur Radius
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -431,9 +431,9 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="100"
|
||||
value={blur}
|
||||
onChange={(e) => setBlur(Math.max(0, Number(e.target.value)))}
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">px</span>
|
||||
<span className="text-xs text-gray-500 font-mono">px</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -442,7 +442,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="100"
|
||||
value={blur}
|
||||
onChange={(e) => setBlur(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-gray-400 font-mono">
|
||||
<span>0px</span>
|
||||
|
|
@ -454,7 +454,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Spread Radius */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Spread Radius
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -464,9 +464,9 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="50"
|
||||
value={spread}
|
||||
onChange={(e) => setSpread(Number(e.target.value))}
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">px</span>
|
||||
<span className="text-xs text-gray-500 font-mono">px</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -475,7 +475,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="50"
|
||||
value={spread}
|
||||
onChange={(e) => setSpread(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-gray-400 font-mono">
|
||||
<span>-50px</span>
|
||||
|
|
@ -486,8 +486,8 @@ export default function BoxShadowGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Controls Column 2: Color, Opacity & Box Appearance */}
|
||||
<div className="space-y-5 p-5 rounded-xl border border-gray-200 dark:border-gray-700/80 bg-gray-50/50 dark:bg-gray-800/50">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<div className="space-y-5 p-5 border border-[#1a1a1a] bg-black/50">
|
||||
<h3 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
|
|
@ -502,11 +502,11 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Shadow Color & Inset Toggle */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Shadow Color
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-shrink-0 w-9 h-9 rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden shadow-inner cursor-pointer">
|
||||
<div className="relative flex-shrink-0 w-9 h-9 border border-[#1a1a1a] overflow-hidden shadow-inner cursor-pointer">
|
||||
<input
|
||||
type="color"
|
||||
value={color}
|
||||
|
|
@ -518,24 +518,24 @@ export default function BoxShadowGeneratorPage() {
|
|||
type="text"
|
||||
value={color}
|
||||
onChange={(e) => setColor(e.target.value)}
|
||||
className="w-full px-2.5 py-1.5 text-xs font-mono rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none uppercase"
|
||||
className="w-full px-2.5 py-1.5 text-xs font-mono border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none uppercase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inset Toggle Checkbox */}
|
||||
<div className="flex flex-col justify-center space-y-1.5 pt-1">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Shadow Type
|
||||
</label>
|
||||
<label className="flex items-center gap-2.5 p-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 cursor-pointer hover:border-blue-500 transition-colors">
|
||||
<label className="flex items-center gap-2.5 p-2 border border-[#1a1a1a] bg-[#0a0a0a] cursor-pointer hover:border-blue-500 transition-colors">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inset}
|
||||
onChange={(e) => setInset(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="w-4 h-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span className="text-xs font-medium text-gray-800 dark:text-gray-200">
|
||||
<span className="text-xs font-medium text-gray-300">
|
||||
Inset Shadow (Inner)
|
||||
</span>
|
||||
</label>
|
||||
|
|
@ -545,7 +545,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Opacity Slider */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Shadow Opacity
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -559,9 +559,9 @@ export default function BoxShadowGeneratorPage() {
|
|||
const val = Math.max(0, Math.min(1, Number(e.target.value)));
|
||||
setOpacity(isNaN(val) ? 0 : val);
|
||||
}}
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-16 px-1.5 py-0.5 text-xs font-mono text-right border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
({Math.round(opacity * 100)}%)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -573,7 +573,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
step="0.01"
|
||||
value={opacity}
|
||||
onChange={(e) => setOpacity(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-gray-400 font-mono">
|
||||
<span>0 (Transparent)</span>
|
||||
|
|
@ -583,14 +583,14 @@ export default function BoxShadowGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Box Appearance: Box Background & Border Radius */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-2 border-t border-[#1a1a1a]">
|
||||
{/* Box Color */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Box Background Color
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-shrink-0 w-8 h-8 rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden shadow-inner cursor-pointer">
|
||||
<div className="relative flex-shrink-0 w-8 h-8 border border-[#1a1a1a] overflow-hidden shadow-inner cursor-pointer">
|
||||
<input
|
||||
type="color"
|
||||
value={boxColor}
|
||||
|
|
@ -602,7 +602,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
type="text"
|
||||
value={boxColor}
|
||||
onChange={(e) => setBoxColor(e.target.value)}
|
||||
className="w-full px-2 py-1 text-xs font-mono rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none uppercase"
|
||||
className="w-full px-2 py-1 text-xs font-mono border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none uppercase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -610,10 +610,10 @@ export default function BoxShadowGeneratorPage() {
|
|||
{/* Border Radius */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-medium text-gray-400">
|
||||
Border Radius
|
||||
</label>
|
||||
<span className="text-xs font-mono text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs font-mono text-gray-500">
|
||||
{borderRadius}px
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -623,7 +623,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
max="50"
|
||||
value={borderRadius}
|
||||
onChange={(e) => setBorderRadius(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -631,20 +631,20 @@ export default function BoxShadowGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Generated CSS Code Block */}
|
||||
<div className="space-y-3 pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<div className="space-y-3 pt-4 border-t border-[#1a1a1a]">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Generated CSS
|
||||
</h2>
|
||||
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 p-0.5 rounded-lg text-xs">
|
||||
<div className="flex items-center gap-1 bg-[#111] p-0.5 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeFormat("standard")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
codeFormat === "standard"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Standard
|
||||
|
|
@ -652,10 +652,10 @@ export default function BoxShadowGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeFormat("full")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
codeFormat === "full"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Full CSS Class
|
||||
|
|
@ -663,10 +663,10 @@ export default function BoxShadowGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeFormat("tailwind")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
codeFormat === "tailwind"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Tailwind
|
||||
|
|
@ -674,12 +674,12 @@ export default function BoxShadowGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{codeFormat !== "tailwind" && (
|
||||
<label className="flex items-center gap-1.5 text-xs text-gray-600 dark:text-gray-400 cursor-pointer ml-2">
|
||||
<label className="flex items-center gap-1.5 text-xs text-gray-600 cursor-pointer ml-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeVendorPrefixes}
|
||||
onChange={(e) => setIncludeVendorPrefixes(e.target.checked)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="rounded border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
Vendor prefixes (-webkit, -moz)
|
||||
</label>
|
||||
|
|
@ -689,7 +689,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer"
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm transition-colors cursor-pointer"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
|
|
@ -715,7 +715,7 @@ export default function BoxShadowGeneratorPage() {
|
|||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<pre className="w-full p-4 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-900 text-gray-100 font-mono text-sm overflow-x-auto leading-relaxed shadow-sm">
|
||||
<pre className="w-full p-4 border border-[#1a1a1a] bg-gray-900 text-gray-100 font-mono text-sm overflow-x-auto leading-relaxed ">
|
||||
<code>{generatedCode}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -293,12 +293,12 @@ export default function CaseConverterPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Top Actions Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-[#1a1a1a]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLoadSample}
|
||||
className="px-3 py-1.5 bg-blue-50 dark:bg-blue-950/40 hover:bg-blue-100 dark:hover:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 bg-[#0a0a1a] hover:bg-[#0a0a1a] text-blue-400 text-xs font-semibold transition-colors cursor-pointer"
|
||||
>
|
||||
Load Sample
|
||||
</button>
|
||||
|
|
@ -306,7 +306,7 @@ export default function CaseConverterPage() {
|
|||
type="button"
|
||||
onClick={handleSwap}
|
||||
disabled={!output && !input}
|
||||
className="px-3 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 text-gray-700 dark:text-gray-200 rounded-lg text-xs font-semibold transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-1.5 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 text-gray-400 text-xs font-semibold transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
|
|
@ -317,20 +317,20 @@ export default function CaseConverterPage() {
|
|||
type="button"
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="px-3 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-rose-50 dark:hover:bg-rose-950/30 disabled:opacity-40 text-gray-700 dark:text-gray-300 hover:text-rose-600 dark:hover:text-rose-400 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 bg-[#111] hover:bg-rose-50 disabled:opacity-40 text-gray-400 hover:text-rose-600 text-xs font-semibold transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="text-xs text-gray-500">
|
||||
Click any case button below to transform immediately
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Case Transformation Buttons Grid */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400">
|
||||
<label className="text-xs font-bold uppercase tracking-wider text-gray-600">
|
||||
Choose Transformation Case
|
||||
</label>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
||||
|
|
@ -341,10 +341,10 @@ export default function CaseConverterPage() {
|
|||
key={key}
|
||||
type="button"
|
||||
onClick={() => handleCaseButtonClick(key)}
|
||||
className={`p-2.5 rounded-lg border text-left transition-all cursor-pointer shadow-xs ${
|
||||
className={`p-2.5 border text-left transition-all cursor-pointer shadow-xs ${
|
||||
isActive
|
||||
? "bg-blue-600 border-blue-600 text-white ring-2 ring-blue-500/40"
|
||||
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100 hover:bg-blue-50/70 dark:hover:bg-blue-950/30 hover:border-blue-300 dark:hover:border-blue-700"
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-200 hover:bg-[#0a0a1a]/70 hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
<div className="font-semibold text-xs truncate">{conv.name}</div>
|
||||
|
|
@ -352,7 +352,7 @@ export default function CaseConverterPage() {
|
|||
className={`text-[10px] font-mono mt-0.5 truncate ${
|
||||
isActive
|
||||
? "text-blue-100"
|
||||
: "text-gray-500 dark:text-gray-400"
|
||||
: "text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{conv.example}
|
||||
|
|
@ -370,11 +370,11 @@ export default function CaseConverterPage() {
|
|||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
className="text-sm font-semibold text-gray-400"
|
||||
>
|
||||
Input Text
|
||||
</label>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{input.length} chars | {input.trim() ? input.trim().split(/\s+/).length : 0} words
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -384,7 +384,7 @@ export default function CaseConverterPage() {
|
|||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Enter or paste text to convert..."
|
||||
rows={10}
|
||||
className="w-full p-3.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
className="w-full p-3.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -394,23 +394,23 @@ export default function CaseConverterPage() {
|
|||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor={outputId}
|
||||
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
className="text-sm font-semibold text-gray-400"
|
||||
>
|
||||
Converted Output
|
||||
</label>
|
||||
<span className="px-2 py-0.5 rounded-full text-xs font-semibold bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300">
|
||||
<span className="px-2 py-0.5 rounded-none text-xs font-semibold bg-[#0a0a1a] text-blue-400">
|
||||
{CONVERTERS[activeCaseKey]?.name || "Result"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{output.length} chars
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
disabled={!output}
|
||||
className="px-2.5 py-1 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 disabled:cursor-not-allowed rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-2.5 py-1 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 disabled:cursor-not-allowed text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
|
|
@ -436,7 +436,7 @@ export default function CaseConverterPage() {
|
|||
readOnly
|
||||
placeholder="Converted result will appear here..."
|
||||
rows={10}
|
||||
className="w-full p-3.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/70 text-gray-900 dark:text-gray-100 font-mono text-sm focus:outline-none resize-y leading-relaxed select-all"
|
||||
className="w-full p-3.5 border border-[#1a1a1a] bg-black text-gray-200 font-mono text-sm focus:outline-none resize-y leading-relaxed select-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -444,10 +444,10 @@ export default function CaseConverterPage() {
|
|||
{/* All Cases Live Preview Cards */}
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="text-sm font-bold text-gray-200">
|
||||
Live Preview of All Cases
|
||||
</h3>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500">
|
||||
Click "Apply" or "Copy" on any case
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -459,19 +459,19 @@ export default function CaseConverterPage() {
|
|||
return (
|
||||
<div
|
||||
key={key}
|
||||
className={`p-3 rounded-lg border transition-all ${
|
||||
className={`p-3 border transition-all ${
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50/40 dark:border-blue-600 dark:bg-blue-950/20"
|
||||
: "border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-850"
|
||||
? "border-blue-500 bg-[#0a0a1a]/40"
|
||||
: "border-[#1a1a1a] bg-[#0a0a0a]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-xs text-gray-900 dark:text-gray-100">
|
||||
<span className="font-semibold text-xs text-gray-200">
|
||||
{conv.name}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<span className="text-[10px] px-1.5 py-0.2 rounded font-bold bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200">
|
||||
<span className="text-[10px] px-1.5 py-0.2 font-bold bg-[#0a0a1a] text-blue-400">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -480,21 +480,21 @@ export default function CaseConverterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => handleCaseButtonClick(key)}
|
||||
className="px-2 py-0.5 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950/40 rounded transition-colors cursor-pointer font-medium"
|
||||
className="px-2 py-0.5 text-xs text-blue-600 hover:bg-[#0a0a1a] transition-colors cursor-pointer font-medium"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopyPreview(convertedVal, key)}
|
||||
className="px-2 py-0.5 text-xs text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer"
|
||||
className="px-2 py-0.5 text-xs text-gray-600 hover:bg-[#111] transition-colors cursor-pointer"
|
||||
>
|
||||
{previewCopiedKey === key ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="font-mono text-xs text-gray-800 dark:text-gray-200 p-2 rounded bg-gray-50 dark:bg-gray-900/60 truncate">
|
||||
<div className="font-mono text-xs text-gray-300 p-2 bg-black truncate">
|
||||
{convertedVal || <span className="text-gray-400 italic">Empty</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ export default function ColorConverterPage() {
|
|||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
|
||||
{/* Large Color Swatch Box */}
|
||||
<div className="lg:col-span-5 flex flex-col">
|
||||
<div className="relative h-56 sm:h-64 w-full rounded-2xl border border-gray-200 dark:border-gray-700 overflow-hidden shadow-inner">
|
||||
<div className="relative h-56 sm:h-64 w-full border border-[#1a1a1a] overflow-hidden shadow-inner">
|
||||
{/* Checkerboard backdrop for transparency */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
|
|
@ -472,20 +472,20 @@ export default function ColorConverterPage() {
|
|||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<span
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-bold shadow-sm backdrop-blur-md bg-white/70 dark:bg-black/70 text-gray-900 dark:text-white"
|
||||
className="inline-flex items-center px-2.5 py-1 text-xs font-bold backdrop-blur-md bg-[#0a0a0a]/70 text-gray-200"
|
||||
>
|
||||
Alpha: {Math.round(color.a * 100)}%
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(currentHex, "swatch-hex")}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-md text-xs font-bold shadow-sm backdrop-blur-md bg-white/70 dark:bg-black/70 text-gray-900 dark:text-white hover:bg-white dark:hover:bg-black transition-colors"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-bold backdrop-blur-md bg-[#0a0a0a]/70 text-gray-200 hover:bg-[#0a0a0a] transition-colors"
|
||||
>
|
||||
{copiedKey === "swatch-hex" ? "Copied!" : currentHex}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-center font-mono font-bold text-lg sm:text-xl drop-shadow-md" style={{ color: contrastWhite >= contrastBlack ? "#FFFFFF" : "#000000" }}>
|
||||
<div className="text-center font-mono font-bold text-lg sm:text-xl drop-" style={{ color: contrastWhite >= contrastBlack ? "#FFFFFF" : "#000000" }}>
|
||||
{cssRgbString}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -500,9 +500,9 @@ export default function ColorConverterPage() {
|
|||
onChange={(e) => handleColorPickerChange(e.target.value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white py-2.5 px-4 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||||
<div className="flex items-center justify-center gap-2 border border-[#1a1a1a] bg-[#0a0a0a] py-2.5 px-4 text-sm font-semibold text-gray-400 hover:bg-black transition-colors">
|
||||
<span
|
||||
className="h-4 w-4 rounded-full border border-gray-400"
|
||||
className="h-4 w-4 rounded-none border border-[#2a2a2a]"
|
||||
style={{ backgroundColor: currentHex }}
|
||||
/>
|
||||
<span>Pick Color (Visual)</span>
|
||||
|
|
@ -512,7 +512,7 @@ export default function ColorConverterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={generateRandomColor}
|
||||
className="flex items-center justify-center gap-1.5 rounded-lg border border-gray-300 bg-white py-2.5 px-4 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
className="flex items-center justify-center gap-1.5 border border-[#1a1a1a] bg-[#0a0a0a] py-2.5 px-4 text-sm font-semibold text-gray-400 hover:bg-black transition-colors"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
|
|
@ -530,53 +530,53 @@ export default function ColorConverterPage() {
|
|||
{/* Right Controls: Sliders & Quick Palettes */}
|
||||
<div className="lg:col-span-7 space-y-4">
|
||||
{/* Channel Sliders */}
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50 space-y-3">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-4 space-y-3">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-gray-600">
|
||||
Channel Controls
|
||||
</h3>
|
||||
|
||||
{/* Red slider */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-12 font-mono text-xs font-bold text-red-600 dark:text-red-400">R: {color.r}</span>
|
||||
<span className="w-12 font-mono text-xs font-bold text-red-400">R: {color.r}</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="255"
|
||||
value={color.r}
|
||||
onChange={(e) => updateAllFromRgba(Number(e.target.value), color.g, color.b, color.a)}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-red-200 dark:bg-red-950 accent-red-600"
|
||||
className="h-2 w-full cursor-pointer appearance-none bg-red-200 accent-red-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Green slider */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-12 font-mono text-xs font-bold text-emerald-600 dark:text-emerald-400">G: {color.g}</span>
|
||||
<span className="w-12 font-mono text-xs font-bold text-emerald-600">G: {color.g}</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="255"
|
||||
value={color.g}
|
||||
onChange={(e) => updateAllFromRgba(color.r, Number(e.target.value), color.b, color.a)}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-emerald-200 dark:bg-emerald-950 accent-emerald-600"
|
||||
className="h-2 w-full cursor-pointer appearance-none bg-emerald-200 accent-emerald-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Blue slider */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-12 font-mono text-xs font-bold text-blue-600 dark:text-blue-400">B: {color.b}</span>
|
||||
<span className="w-12 font-mono text-xs font-bold text-blue-600">B: {color.b}</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="255"
|
||||
value={color.b}
|
||||
onChange={(e) => updateAllFromRgba(color.r, color.g, Number(e.target.value), color.a)}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-blue-200 dark:bg-blue-950 accent-blue-600"
|
||||
className="h-2 w-full cursor-pointer appearance-none bg-blue-200 accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Alpha slider */}
|
||||
<div className="flex items-center gap-3 pt-1 border-t border-gray-200 dark:border-gray-800">
|
||||
<span className="w-12 font-mono text-xs font-bold text-gray-700 dark:text-gray-300">
|
||||
<div className="flex items-center gap-3 pt-1 border-t border-[#1a1a1a]">
|
||||
<span className="w-12 font-mono text-xs font-bold text-gray-400">
|
||||
A: {Math.round(color.a * 100)}%
|
||||
</span>
|
||||
<input
|
||||
|
|
@ -586,14 +586,14 @@ export default function ColorConverterPage() {
|
|||
step="0.01"
|
||||
value={color.a}
|
||||
onChange={(e) => handleAlphaChange(parseFloat(e.target.value))}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-300 dark:bg-gray-700 accent-gray-800 dark:accent-gray-200"
|
||||
className="h-2 w-full cursor-pointer appearance-none bg-[#222] accent-gray-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Preset Colors */}
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-600 dark:text-gray-400 block mb-2">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-4">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-600 block mb-2">
|
||||
Preset Palettes
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
|
@ -605,11 +605,11 @@ export default function ColorConverterPage() {
|
|||
const p = parseHex(preset.hex);
|
||||
if (p) updateAllFromRgba(p.r, p.g, p.b, 1);
|
||||
}}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:scale-105 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 transition-all"
|
||||
className="flex items-center gap-1.5 border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 text-xs font-medium text-gray-400 hover:scale-105 transition-all"
|
||||
title={preset.name}
|
||||
>
|
||||
<span
|
||||
className="h-3.5 w-3.5 rounded-full border border-black/10"
|
||||
className="h-3.5 w-3.5 rounded-none border border-black/10"
|
||||
style={{ backgroundColor: preset.hex }}
|
||||
/>
|
||||
<span>{preset.name}</span>
|
||||
|
|
@ -622,25 +622,25 @@ export default function ColorConverterPage() {
|
|||
|
||||
{/* Live Two-Way Formats Conversion Section */}
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
<h2 className="text-lg font-bold text-gray-200">
|
||||
Color Formats & Two-Way Editors
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-gray-500">
|
||||
Edit any input below to convert instantly into all other formats.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{/* HEX Input Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label htmlFor="hex-field" className="text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor="hex-field" className="text-xs font-bold uppercase tracking-wider text-gray-400">
|
||||
HEX Value
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(currentHex, "fmt-hex")}
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-400 flex items-center gap-1"
|
||||
>
|
||||
{copiedKey === "fmt-hex" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
|
|
@ -651,16 +651,16 @@ export default function ColorConverterPage() {
|
|||
value={hexInput}
|
||||
onChange={(e) => handleHexChange(e.target.value)}
|
||||
placeholder="#FF5733"
|
||||
className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-3 font-mono text-base border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
{inputErrors.hex && <p className="mt-1 text-xs text-red-500">{inputErrors.hex}</p>}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 pt-2 border-t border-gray-100">
|
||||
<span>8-digit HEX (with Alpha):</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(currentHex8, "fmt-hex8")}
|
||||
className="font-mono text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-semibold"
|
||||
className="font-mono text-gray-300 hover:text-blue-600 font-semibold"
|
||||
>
|
||||
{copiedKey === "fmt-hex8" ? "Copied!" : currentHex8}
|
||||
</button>
|
||||
|
|
@ -668,16 +668,16 @@ export default function ColorConverterPage() {
|
|||
</div>
|
||||
|
||||
{/* RGB Input Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label htmlFor="rgb-field" className="text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor="rgb-field" className="text-xs font-bold uppercase tracking-wider text-gray-400">
|
||||
RGB Value
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(cssRgbString, "fmt-rgb")}
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-400 flex items-center gap-1"
|
||||
>
|
||||
{copiedKey === "fmt-rgb" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
|
|
@ -688,29 +688,29 @@ export default function ColorConverterPage() {
|
|||
value={rgbInput}
|
||||
onChange={(e) => handleRgbChange(e.target.value)}
|
||||
placeholder="rgb(255, 87, 51)"
|
||||
className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-3 font-mono text-base border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
{inputErrors.rgb && <p className="mt-1 text-xs text-red-500">{inputErrors.rgb}</p>}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 pt-2 border-t border-gray-100">
|
||||
<span>Array values:</span>
|
||||
<span className="font-mono text-gray-800 dark:text-gray-200">
|
||||
<span className="font-mono text-gray-300">
|
||||
[{color.r}, {color.g}, {color.b}]
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RGBA Input Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label htmlFor="rgba-field" className="text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor="rgba-field" className="text-xs font-bold uppercase tracking-wider text-gray-400">
|
||||
RGBA Value (with Alpha)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(cssRgbaString, "fmt-rgba")}
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-400 flex items-center gap-1"
|
||||
>
|
||||
{copiedKey === "fmt-rgba" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
|
|
@ -721,27 +721,27 @@ export default function ColorConverterPage() {
|
|||
value={rgbaInput}
|
||||
onChange={(e) => handleRgbaChange(e.target.value)}
|
||||
placeholder="rgba(255, 87, 51, 1)"
|
||||
className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-3 font-mono text-base border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
{inputErrors.rgba && <p className="mt-1 text-xs text-red-500">{inputErrors.rgba}</p>}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 pt-2 border-t border-gray-100">
|
||||
<span>CSS Output:</span>
|
||||
<span className="font-mono text-gray-800 dark:text-gray-200 truncate ml-2">{cssRgbaString}</span>
|
||||
<span className="font-mono text-gray-300 truncate ml-2">{cssRgbaString}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* HSL Input Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label htmlFor="hsl-field" className="text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor="hsl-field" className="text-xs font-bold uppercase tracking-wider text-gray-400">
|
||||
HSL Value
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(cssHslString, "fmt-hsl")}
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center gap-1"
|
||||
className="text-xs font-medium text-blue-600 hover:text-blue-400 flex items-center gap-1"
|
||||
>
|
||||
{copiedKey === "fmt-hsl" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
|
|
@ -752,16 +752,16 @@ export default function ColorConverterPage() {
|
|||
value={hslInput}
|
||||
onChange={(e) => handleHslChange(e.target.value)}
|
||||
placeholder="hsl(11, 100%, 60%)"
|
||||
className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-3 font-mono text-base border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
{inputErrors.hsl && <p className="mt-1 text-xs text-red-500">{inputErrors.hsl}</p>}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-gray-500 pt-2 border-t border-gray-100">
|
||||
<span>HSLA (with Alpha):</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(cssHslaString, "fmt-hsla")}
|
||||
className="font-mono text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-semibold"
|
||||
className="font-mono text-gray-300 hover:text-blue-600 font-semibold"
|
||||
>
|
||||
{copiedKey === "fmt-hsla" ? "Copied!" : cssHslaString}
|
||||
</button>
|
||||
|
|
@ -773,38 +773,38 @@ export default function ColorConverterPage() {
|
|||
{/* Additional Formats (HSV & CMYK) */}
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{/* HSV Card */}
|
||||
<div className="flex items-center justify-between rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between border border-[#1a1a1a] bg-black p-4">
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400 block mb-1">
|
||||
<span className="text-xs font-semibold text-gray-500 block mb-1">
|
||||
HSV / HSB Format
|
||||
</span>
|
||||
<span className="font-mono text-sm font-bold text-gray-900 dark:text-white">
|
||||
<span className="font-mono text-sm font-bold text-gray-200">
|
||||
{cssHsvString}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(cssHsvString, "fmt-hsv")}
|
||||
className="rounded bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1.5 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
{copiedKey === "fmt-hsv" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CMYK Card */}
|
||||
<div className="flex items-center justify-between rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between border border-[#1a1a1a] bg-black p-4">
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400 block mb-1">
|
||||
<span className="text-xs font-semibold text-gray-500 block mb-1">
|
||||
CMYK Format (Print)
|
||||
</span>
|
||||
<span className="font-mono text-sm font-bold text-gray-900 dark:text-white">
|
||||
<span className="font-mono text-sm font-bold text-gray-200">
|
||||
{cssCmykString}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(cssCmykString, "fmt-cmyk")}
|
||||
className="rounded bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1.5 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
{copiedKey === "fmt-cmyk" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
|
|
@ -812,41 +812,41 @@ export default function ColorConverterPage() {
|
|||
</div>
|
||||
|
||||
{/* Accessibility & Contrast Info */}
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-800 dark:bg-gray-900/50 space-y-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-800 dark:text-gray-200">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-5 space-y-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-300">
|
||||
WCAG Accessibility Contrast
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{/* Contrast with White */}
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold text-gray-600 dark:text-gray-300">White Text (#FFFFFF)</span>
|
||||
<span className="font-mono text-sm font-bold text-gray-900 dark:text-white">
|
||||
<span className="text-xs font-semibold text-gray-600">White Text (#FFFFFF)</span>
|
||||
<span className="font-mono text-sm font-bold text-gray-200">
|
||||
{contrastWhite.toFixed(2)}:1
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-10 rounded-md flex items-center justify-center font-semibold text-white text-sm shadow-inner"
|
||||
className="h-10 flex items-center justify-center font-semibold text-white text-sm shadow-inner"
|
||||
style={{ backgroundColor: currentHex }}
|
||||
>
|
||||
Sample White Text
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2 text-xs">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded font-medium ${
|
||||
className={`px-2 py-0.5 font-medium ${
|
||||
contrastWhite >= 4.5
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "bg-[#1a0a0a] text-red-400"
|
||||
}`}
|
||||
>
|
||||
AA Normal {contrastWhite >= 4.5 ? "✓ Pass" : "✕ Fail"}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded font-medium ${
|
||||
className={`px-2 py-0.5 font-medium ${
|
||||
contrastWhite >= 7
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "bg-[#1a0a0a] text-red-400"
|
||||
}`}
|
||||
>
|
||||
AAA {contrastWhite >= 7 ? "✓ Pass" : "✕ Fail"}
|
||||
|
|
@ -855,34 +855,34 @@ export default function ColorConverterPage() {
|
|||
</div>
|
||||
|
||||
{/* Contrast with Black */}
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold text-gray-600 dark:text-gray-300">Black Text (#000000)</span>
|
||||
<span className="font-mono text-sm font-bold text-gray-900 dark:text-white">
|
||||
<span className="text-xs font-semibold text-gray-600">Black Text (#000000)</span>
|
||||
<span className="font-mono text-sm font-bold text-gray-200">
|
||||
{contrastBlack.toFixed(2)}:1
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-10 rounded-md flex items-center justify-center font-semibold text-black text-sm shadow-inner"
|
||||
className="h-10 flex items-center justify-center font-semibold text-black text-sm shadow-inner"
|
||||
style={{ backgroundColor: currentHex }}
|
||||
>
|
||||
Sample Black Text
|
||||
</div>
|
||||
<div className="mt-2 flex gap-2 text-xs">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded font-medium ${
|
||||
className={`px-2 py-0.5 font-medium ${
|
||||
contrastBlack >= 4.5
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "bg-[#1a0a0a] text-red-400"
|
||||
}`}
|
||||
>
|
||||
AA Normal {contrastBlack >= 4.5 ? "✓ Pass" : "✕ Fail"}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded font-medium ${
|
||||
className={`px-2 py-0.5 font-medium ${
|
||||
contrastBlack >= 7
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
|
||||
: "bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300"
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "bg-[#1a0a0a] text-red-400"
|
||||
}`}
|
||||
>
|
||||
AAA {contrastBlack >= 7 ? "✓ Pass" : "✕ Fail"}
|
||||
|
|
@ -893,27 +893,27 @@ export default function ColorConverterPage() {
|
|||
</div>
|
||||
|
||||
{/* Color Harmonies */}
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-800 dark:bg-gray-900/50 space-y-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-800 dark:text-gray-200">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-5 space-y-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-300">
|
||||
Harmonies & Shades (Click to load)
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{/* Complementary */}
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 block mb-1.5">
|
||||
<span className="text-xs font-medium text-gray-500 block mb-1.5">
|
||||
Complementary
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateAllFromRgba(harmonies.complementary.rgb.r, harmonies.complementary.rgb.g, harmonies.complementary.rgb.b, 1)}
|
||||
className="w-full flex items-center gap-2 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
|
||||
className="w-full flex items-center gap-2 p-2 bg-[#0a0a0a] border border-[#1a1a1a] hover:scale-[1.02] transition-transform text-left"
|
||||
>
|
||||
<span
|
||||
className="h-8 w-8 rounded-md border border-black/10 shrink-0"
|
||||
className="h-8 w-8 border border-black/10 shrink-0"
|
||||
style={{ backgroundColor: harmonies.complementary.hex }}
|
||||
/>
|
||||
<span className="font-mono text-xs font-medium text-gray-800 dark:text-gray-200 truncate">
|
||||
<span className="font-mono text-xs font-medium text-gray-300 truncate">
|
||||
{harmonies.complementary.hex}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -921,33 +921,33 @@ export default function ColorConverterPage() {
|
|||
|
||||
{/* Analogous */}
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 block mb-1.5">
|
||||
<span className="text-xs font-medium text-gray-500 block mb-1.5">
|
||||
Analogous
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateAllFromRgba(harmonies.analogous1.rgb.r, harmonies.analogous1.rgb.g, harmonies.analogous1.rgb.b, 1)}
|
||||
className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
|
||||
className="flex-1 flex items-center gap-1.5 p-2 bg-[#0a0a0a] border border-[#1a1a1a] hover:scale-[1.02] transition-transform text-left"
|
||||
>
|
||||
<span
|
||||
className="h-6 w-6 rounded-md border border-black/10 shrink-0"
|
||||
className="h-6 w-6 border border-black/10 shrink-0"
|
||||
style={{ backgroundColor: harmonies.analogous1.hex }}
|
||||
/>
|
||||
<span className="font-mono text-xs font-medium text-gray-800 dark:text-gray-200 truncate">
|
||||
<span className="font-mono text-xs font-medium text-gray-300 truncate">
|
||||
{harmonies.analogous1.hex}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateAllFromRgba(harmonies.analogous2.rgb.r, harmonies.analogous2.rgb.g, harmonies.analogous2.rgb.b, 1)}
|
||||
className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
|
||||
className="flex-1 flex items-center gap-1.5 p-2 bg-[#0a0a0a] border border-[#1a1a1a] hover:scale-[1.02] transition-transform text-left"
|
||||
>
|
||||
<span
|
||||
className="h-6 w-6 rounded-md border border-black/10 shrink-0"
|
||||
className="h-6 w-6 border border-black/10 shrink-0"
|
||||
style={{ backgroundColor: harmonies.analogous2.hex }}
|
||||
/>
|
||||
<span className="font-mono text-xs font-medium text-gray-800 dark:text-gray-200 truncate">
|
||||
<span className="font-mono text-xs font-medium text-gray-300 truncate">
|
||||
{harmonies.analogous2.hex}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -956,33 +956,33 @@ export default function ColorConverterPage() {
|
|||
|
||||
{/* Triadic */}
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 block mb-1.5">
|
||||
<span className="text-xs font-medium text-gray-500 block mb-1.5">
|
||||
Triadic
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateAllFromRgba(harmonies.triadic1.rgb.r, harmonies.triadic1.rgb.g, harmonies.triadic1.rgb.b, 1)}
|
||||
className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
|
||||
className="flex-1 flex items-center gap-1.5 p-2 bg-[#0a0a0a] border border-[#1a1a1a] hover:scale-[1.02] transition-transform text-left"
|
||||
>
|
||||
<span
|
||||
className="h-6 w-6 rounded-md border border-black/10 shrink-0"
|
||||
className="h-6 w-6 border border-black/10 shrink-0"
|
||||
style={{ backgroundColor: harmonies.triadic1.hex }}
|
||||
/>
|
||||
<span className="font-mono text-xs font-medium text-gray-800 dark:text-gray-200 truncate">
|
||||
<span className="font-mono text-xs font-medium text-gray-300 truncate">
|
||||
{harmonies.triadic1.hex}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateAllFromRgba(harmonies.triadic2.rgb.r, harmonies.triadic2.rgb.g, harmonies.triadic2.rgb.b, 1)}
|
||||
className="flex-1 flex items-center gap-1.5 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:scale-[1.02] transition-transform text-left"
|
||||
className="flex-1 flex items-center gap-1.5 p-2 bg-[#0a0a0a] border border-[#1a1a1a] hover:scale-[1.02] transition-transform text-left"
|
||||
>
|
||||
<span
|
||||
className="h-6 w-6 rounded-md border border-black/10 shrink-0"
|
||||
className="h-6 w-6 border border-black/10 shrink-0"
|
||||
style={{ backgroundColor: harmonies.triadic2.hex }}
|
||||
/>
|
||||
<span className="font-mono text-xs font-medium text-gray-800 dark:text-gray-200 truncate">
|
||||
<span className="font-mono text-xs font-medium text-gray-300 truncate">
|
||||
{harmonies.triadic2.hex}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -992,7 +992,7 @@ export default function ColorConverterPage() {
|
|||
|
||||
{/* Tints & Shades scale */}
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 block mb-1.5">
|
||||
<span className="text-xs font-medium text-gray-500 block mb-1.5">
|
||||
Lightness Spectrum (15% to 90%)
|
||||
</span>
|
||||
<div className="grid grid-cols-6 gap-1.5">
|
||||
|
|
@ -1001,7 +1001,7 @@ export default function ColorConverterPage() {
|
|||
key={tint.lightness}
|
||||
type="button"
|
||||
onClick={() => updateAllFromRgba(tint.rgb.r, tint.rgb.g, tint.rgb.b, 1)}
|
||||
className="h-10 rounded-md border border-black/10 hover:scale-105 transition-transform flex items-center justify-center"
|
||||
className="h-10 border border-black/10 hover:scale-105 transition-transform flex items-center justify-center"
|
||||
style={{ backgroundColor: tint.hex }}
|
||||
title={`${tint.hex} (${tint.lightness}%)`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -680,9 +680,9 @@ export default function CronParserPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Top Preset Gallery */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-gray-50/70 dark:bg-gray-900/50 p-4 space-y-3 shadow-xs">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-gray-200/80 dark:border-gray-800 pb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-4 space-y-3 shadow-xs">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-[#1a1a1a]/80 pb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-400">
|
||||
Common Cron Presets
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">Click any preset to load</span>
|
||||
|
|
@ -696,18 +696,18 @@ export default function CronParserPage() {
|
|||
key={p.cron + p.name}
|
||||
type="button"
|
||||
onClick={() => setCronExpression(p.cron)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs transition-all cursor-pointer flex items-center gap-2 border ${
|
||||
className={`px-3 py-1.5 text-xs transition-all cursor-pointer flex items-center gap-2 border ${
|
||||
isSelected
|
||||
? "bg-blue-600 border-blue-600 text-white font-semibold shadow-xs"
|
||||
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-blue-50 dark:hover:bg-blue-950/40 hover:border-blue-300 dark:hover:border-blue-700"
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-400 hover:bg-[#0a0a1a] hover:border-blue-300"
|
||||
}`}
|
||||
>
|
||||
<span>{p.name}</span>
|
||||
<code
|
||||
className={`font-mono text-[11px] px-1 py-0.5 rounded ${
|
||||
className={`font-mono text-[11px] px-1 py-0.5 ${
|
||||
isSelected
|
||||
? "bg-blue-700 text-blue-100"
|
||||
: "bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400"
|
||||
: "bg-[#111] text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{p.cron}
|
||||
|
|
@ -719,9 +719,9 @@ export default function CronParserPage() {
|
|||
</div>
|
||||
|
||||
{/* Main Cron Input Bar */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 shadow-xs space-y-4">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<label htmlFor="cron-main-input" className="text-sm font-bold text-gray-900 dark:text-white">
|
||||
<label htmlFor="cron-main-input" className="text-sm font-bold text-gray-200">
|
||||
Enter Cron Expression (5 fields)
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -729,7 +729,7 @@ export default function CronParserPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopyCron}
|
||||
className="px-2.5 py-1 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copiedCron ? "Copied!" : "Copy Expression"}
|
||||
</button>
|
||||
|
|
@ -743,18 +743,18 @@ export default function CronParserPage() {
|
|||
value={cronExpression}
|
||||
onChange={(e) => setCronExpression(e.target.value)}
|
||||
placeholder="* * * * *"
|
||||
className="w-full p-4 font-mono text-xl sm:text-2xl tracking-wider rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none shadow-inner"
|
||||
className="w-full p-4 font-mono text-xl sm:text-2xl tracking-wider border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Human Readable Explanation Banner */}
|
||||
{parsedCron.valid ? (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20 text-emerald-900 dark:text-emerald-200">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-emerald-500 text-white text-xs font-bold mt-0.5">
|
||||
<div className="flex items-start gap-3 p-4 border border-emerald-200 bg-emerald-50/70 text-emerald-900">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-none bg-emerald-500 text-white text-xs font-bold mt-0.5">
|
||||
✓
|
||||
</span>
|
||||
<div>
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-emerald-700 dark:text-emerald-400 block">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-emerald-700 block">
|
||||
Human-Readable Schedule
|
||||
</span>
|
||||
<p className="mt-0.5 text-base sm:text-lg font-bold">
|
||||
|
|
@ -763,12 +763,12 @@ export default function CronParserPage() {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-3 p-4 rounded-xl border border-rose-200 bg-rose-50/80 dark:border-rose-900/50 dark:bg-rose-950/30 text-rose-900 dark:text-rose-200">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-rose-500 text-white text-xs font-bold mt-0.5">
|
||||
<div className="flex items-start gap-3 p-4 border border-rose-200 bg-rose-50/80 text-rose-900">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-none bg-rose-500 text-white text-xs font-bold mt-0.5">
|
||||
!
|
||||
</span>
|
||||
<div>
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-rose-700 dark:text-rose-400 block">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-rose-700 block">
|
||||
Syntax Error
|
||||
</span>
|
||||
<p className="mt-0.5 text-sm font-medium">{parsedCron.error}</p>
|
||||
|
|
@ -780,7 +780,7 @@ export default function CronParserPage() {
|
|||
{/* 5 Field Breakdown Interactive Cards */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="text-sm font-bold text-gray-200">
|
||||
Cron Field Breakdown & Structure
|
||||
</h3>
|
||||
<span className="text-xs text-gray-400">Edit individual fields directly below</span>
|
||||
|
|
@ -795,15 +795,15 @@ export default function CronParserPage() {
|
|||
return (
|
||||
<div
|
||||
key={fDef.key}
|
||||
className={`flex flex-col justify-between p-3.5 rounded-xl border transition-all ${
|
||||
className={`flex flex-col justify-between p-3.5 border transition-all ${
|
||||
isValid
|
||||
? "border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-850"
|
||||
: "border-rose-300 dark:border-rose-800 bg-rose-50/40 dark:bg-rose-950/20"
|
||||
? "border-[#1a1a1a] bg-[#0a0a0a]"
|
||||
: "border-rose-300 bg-rose-50/40"
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-800 dark:text-gray-200 uppercase tracking-wide">
|
||||
<span className="text-xs font-bold text-gray-300 uppercase tracking-wide">
|
||||
{fDef.name}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono text-gray-400">
|
||||
|
|
@ -816,17 +816,17 @@ export default function CronParserPage() {
|
|||
value={rawToken}
|
||||
onChange={(e) => handleFieldChange(idx, e.target.value)}
|
||||
placeholder="*"
|
||||
className="w-full p-2 font-mono text-base font-bold text-center rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2 font-mono text-base font-bold text-center border border-[#1a1a1a] bg-black text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
|
||||
<p className="text-[11px] text-gray-500 dark:text-gray-400 leading-tight">
|
||||
<p className="text-[11px] text-gray-500 leading-tight">
|
||||
{fDef.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{parsedF && parsedF.valid && (
|
||||
<div className="mt-3 pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400 block truncate">
|
||||
<div className="mt-3 pt-2 border-t border-gray-100">
|
||||
<span className="text-[11px] font-medium text-blue-600 block truncate">
|
||||
{parsedF.explanation}
|
||||
</span>
|
||||
<div className="text-[10px] text-gray-400 font-mono mt-0.5 truncate" title={parsedF.values.join(", ")}>
|
||||
|
|
@ -841,13 +841,13 @@ export default function CronParserPage() {
|
|||
</div>
|
||||
|
||||
{/* Scheduled Runs Section */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 shadow-xs space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-100 dark:border-gray-800 pb-3">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-100 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 text-xs font-bold">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-none bg-[#0a0a1a] text-blue-400 text-xs font-bold">
|
||||
⏰
|
||||
</span>
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-white">
|
||||
<h3 className="text-sm font-bold text-gray-200">
|
||||
Upcoming Scheduled Runs (Next {runCount})
|
||||
</h3>
|
||||
</div>
|
||||
|
|
@ -855,15 +855,15 @@ export default function CronParserPage() {
|
|||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Timezone Toggle */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400">Timezone:</span>
|
||||
<div className="inline-flex rounded-lg border border-gray-300 dark:border-gray-700 p-0.5 bg-gray-100 dark:bg-gray-800">
|
||||
<span className="text-gray-500">Timezone:</span>
|
||||
<div className="inline-flex border border-[#1a1a1a] p-0.5 bg-[#111]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTimezone("local")}
|
||||
className={`px-2 py-0.5 rounded text-xs font-medium transition-colors cursor-pointer ${
|
||||
className={`px-2 py-0.5 text-xs font-medium transition-colors cursor-pointer ${
|
||||
timezone === "local"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Local Time
|
||||
|
|
@ -871,10 +871,10 @@ export default function CronParserPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setTimezone("utc")}
|
||||
className={`px-2 py-0.5 rounded text-xs font-medium transition-colors cursor-pointer ${
|
||||
className={`px-2 py-0.5 text-xs font-medium transition-colors cursor-pointer ${
|
||||
timezone === "utc"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
UTC
|
||||
|
|
@ -884,11 +884,11 @@ export default function CronParserPage() {
|
|||
|
||||
{/* Count selector */}
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400">Count:</span>
|
||||
<span className="text-gray-500">Count:</span>
|
||||
<select
|
||||
value={runCount}
|
||||
onChange={(e) => setRunCount(Number(e.target.value))}
|
||||
className="p-1 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-xs focus:outline-none"
|
||||
className="p-1 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-xs focus:outline-none"
|
||||
>
|
||||
<option value={5}>5 runs</option>
|
||||
<option value={8}>8 runs</option>
|
||||
|
|
@ -901,7 +901,7 @@ export default function CronParserPage() {
|
|||
type="button"
|
||||
onClick={handleCopySchedule}
|
||||
disabled={nextRuns.length === 0}
|
||||
className="px-3 py-1 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 disabled:opacity-40 rounded text-xs font-medium transition-colors cursor-pointer"
|
||||
className="px-3 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 disabled:opacity-40 text-xs font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
{copiedSchedule ? "Copied List!" : "Copy Schedule"}
|
||||
</button>
|
||||
|
|
@ -909,7 +909,7 @@ export default function CronParserPage() {
|
|||
</div>
|
||||
|
||||
{nextRuns.length > 0 ? (
|
||||
<div className="divide-y divide-gray-100 dark:divide-gray-800 max-h-[420px] overflow-y-auto">
|
||||
<div className="divide-y divide-gray-100 max-h-[420px] overflow-y-auto">
|
||||
{nextRuns.map((date, idx) => {
|
||||
const relative = getRelativeTime(date, now);
|
||||
const isLocal = timezone === "local";
|
||||
|
|
@ -934,24 +934,24 @@ export default function CronParserPage() {
|
|||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between py-2.5 px-2 hover:bg-gray-50 dark:hover:bg-gray-800/40 rounded-lg transition-colors text-xs"
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between py-2.5 px-2 hover:bg-black transition-colors text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800 text-[11px] font-mono font-bold text-gray-500 dark:text-gray-400">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-none bg-[#111] text-[11px] font-mono font-bold text-gray-500">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<div>
|
||||
<span className="font-semibold text-gray-900 dark:text-white mr-2">
|
||||
<span className="font-semibold text-gray-200 mr-2">
|
||||
{dateStr}
|
||||
</span>
|
||||
<span className="font-mono text-gray-600 dark:text-gray-300">
|
||||
<span className="font-mono text-gray-600">
|
||||
{timeStr}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-1 sm:mt-0 flex items-center gap-3">
|
||||
<span className="px-2 py-0.5 rounded-full bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300 font-medium">
|
||||
<span className="px-2 py-0.5 rounded-none bg-[#0a0a1a] text-blue-400 font-medium">
|
||||
{relative}
|
||||
</span>
|
||||
<span className="font-mono text-[11px] text-gray-400 hidden md:inline">
|
||||
|
|
@ -970,41 +970,41 @@ export default function CronParserPage() {
|
|||
</div>
|
||||
|
||||
{/* Cron Syntax Cheat Sheet */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800 space-y-4">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-white">
|
||||
<div className="mt-8 pt-6 border-t border-[#1a1a1a] space-y-4">
|
||||
<h3 className="text-sm font-bold text-gray-200">
|
||||
Cron Syntax Reference & Special Characters
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 text-xs">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-850/60 rounded-xl border border-gray-200 dark:border-gray-800 space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 dark:text-blue-400 text-sm">* (Asterisk)</div>
|
||||
<p className="font-semibold text-gray-800 dark:text-gray-200">Wildcard (Any Value)</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Matches every possible value in the field. E.g. <code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">*</code> in hour means every hour.
|
||||
<div className="p-3 bg-black border border-[#1a1a1a] space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 text-sm">* (Asterisk)</div>
|
||||
<p className="font-semibold text-gray-300">Wildcard (Any Value)</p>
|
||||
<p className="text-gray-500">
|
||||
Matches every possible value in the field. E.g. <code className="bg-[#1a1a1a] px-1 rounded">*</code> in hour means every hour.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-850/60 rounded-xl border border-gray-200 dark:border-gray-800 space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 dark:text-blue-400 text-sm">, (Comma)</div>
|
||||
<p className="font-semibold text-gray-800 dark:text-gray-200">Value List</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Specifies discrete multiple values. E.g. <code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">1,15,30</code> in minute runs at minutes 1, 15, 30.
|
||||
<div className="p-3 bg-black border border-[#1a1a1a] space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 text-sm">, (Comma)</div>
|
||||
<p className="font-semibold text-gray-300">Value List</p>
|
||||
<p className="text-gray-500">
|
||||
Specifies discrete multiple values. E.g. <code className="bg-[#1a1a1a] px-1 rounded">1,15,30</code> in minute runs at minutes 1, 15, 30.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-850/60 rounded-xl border border-gray-200 dark:border-gray-800 space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 dark:text-blue-400 text-sm">- (Hyphen)</div>
|
||||
<p className="font-semibold text-gray-800 dark:text-gray-200">Range of Values</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Specifies an inclusive range. E.g. <code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">1-5</code> in weekday means Monday through Friday.
|
||||
<div className="p-3 bg-black border border-[#1a1a1a] space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 text-sm">- (Hyphen)</div>
|
||||
<p className="font-semibold text-gray-300">Range of Values</p>
|
||||
<p className="text-gray-500">
|
||||
Specifies an inclusive range. E.g. <code className="bg-[#1a1a1a] px-1 rounded">1-5</code> in weekday means Monday through Friday.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-850/60 rounded-xl border border-gray-200 dark:border-gray-800 space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 dark:text-blue-400 text-sm">/ (Slash)</div>
|
||||
<p className="font-semibold text-gray-800 dark:text-gray-200">Step Values</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Specifies step increments. E.g. <code className="bg-gray-200 dark:bg-gray-700 px-1 rounded">*/15</code> in minute runs every 15 minutes.
|
||||
<div className="p-3 bg-black border border-[#1a1a1a] space-y-1">
|
||||
<div className="font-mono font-bold text-blue-600 text-sm">/ (Slash)</div>
|
||||
<p className="font-semibold text-gray-300">Step Values</p>
|
||||
<p className="text-gray-500">
|
||||
Specifies step increments. E.g. <code className="bg-[#1a1a1a] px-1 rounded">*/15</code> in minute runs every 15 minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -281,23 +281,23 @@ export default function CssGradientGeneratorPage() {
|
|||
{/* Live Preview Area */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Live Preview
|
||||
</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-gray-600 dark:text-gray-300 cursor-pointer">
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-gray-600 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={previewText}
|
||||
onChange={(e) => setPreviewText(e.target.checked)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="rounded border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
Show Text Overlay
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={generateRandomGradient}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-400 bg-[#111] hover:bg-[#1a1a1a] transition-colors"
|
||||
title="Generate Random Gradient"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -314,12 +314,12 @@ export default function CssGradientGeneratorPage() {
|
|||
</div>
|
||||
|
||||
<div
|
||||
className="relative w-full h-64 sm:h-72 rounded-2xl border border-gray-200 dark:border-gray-700 shadow-inner flex items-center justify-center p-6 transition-all duration-300 overflow-hidden"
|
||||
className="relative w-full h-64 sm:h-72 border border-[#1a1a1a] shadow-inner flex items-center justify-center p-6 transition-all duration-300 overflow-hidden"
|
||||
style={{ background: gradientValue }}
|
||||
>
|
||||
{previewText && (
|
||||
<div className="bg-black/30 backdrop-blur-md text-white rounded-xl p-5 max-w-md text-center shadow-lg border border-white/10 transition-all">
|
||||
<h3 className="text-xl sm:text-2xl font-bold tracking-tight mb-1 drop-shadow-sm">
|
||||
<div className="bg-black/30 backdrop-blur-md text-white p-5 max-w-md text-center border border-white/10 transition-all">
|
||||
<h3 className="text-xl sm:text-2xl font-bold tracking-tight mb-1 drop-">
|
||||
CSS Gradient Preview
|
||||
</h3>
|
||||
<p className="text-xs sm:text-sm text-gray-100/90 font-mono">
|
||||
|
|
@ -332,7 +332,7 @@ export default function CssGradientGeneratorPage() {
|
|||
|
||||
{/* Presets Gallery */}
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Quick Presets
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-8 gap-2.5">
|
||||
|
|
@ -346,13 +346,13 @@ export default function CssGradientGeneratorPage() {
|
|||
key={preset.name}
|
||||
type="button"
|
||||
onClick={() => applyPreset(preset)}
|
||||
className="group relative flex flex-col items-center gap-1.5 p-1.5 rounded-xl border border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 bg-white dark:bg-gray-800 transition-all hover:scale-[1.03] text-left"
|
||||
className="group relative flex flex-col items-center gap-1.5 p-1.5 border border-[#1a1a1a] hover:border-blue-500 bg-[#0a0a0a] transition-all hover:scale-[1.03] text-left"
|
||||
>
|
||||
<div
|
||||
className="w-full h-12 rounded-lg shadow-sm"
|
||||
className="w-full h-12 "
|
||||
style={{ background: bgString }}
|
||||
/>
|
||||
<span className="text-[11px] font-medium text-gray-700 dark:text-gray-300 truncate w-full text-center">
|
||||
<span className="text-[11px] font-medium text-gray-400 truncate w-full text-center">
|
||||
{preset.name}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -367,17 +367,17 @@ export default function CssGradientGeneratorPage() {
|
|||
<div className="lg:col-span-5 space-y-6">
|
||||
{/* Gradient Type */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<label className="text-sm font-medium text-gray-200">
|
||||
Gradient Type
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setGradientType("linear")}
|
||||
className={`py-2.5 px-4 text-sm font-medium rounded-lg border transition-all ${
|
||||
className={`py-2.5 px-4 text-sm font-medium border transition-all ${
|
||||
gradientType === "linear"
|
||||
? "bg-blue-600 border-blue-600 text-white shadow-sm"
|
||||
: "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50"
|
||||
? "bg-blue-600 border-blue-600 text-white "
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-400 hover:bg-black"
|
||||
}`}
|
||||
>
|
||||
Linear
|
||||
|
|
@ -385,10 +385,10 @@ export default function CssGradientGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setGradientType("radial")}
|
||||
className={`py-2.5 px-4 text-sm font-medium rounded-lg border transition-all ${
|
||||
className={`py-2.5 px-4 text-sm font-medium border transition-all ${
|
||||
gradientType === "radial"
|
||||
? "bg-blue-600 border-blue-600 text-white shadow-sm"
|
||||
: "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50"
|
||||
? "bg-blue-600 border-blue-600 text-white "
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-400 hover:bg-black"
|
||||
}`}
|
||||
>
|
||||
Radial
|
||||
|
|
@ -401,8 +401,8 @@ export default function CssGradientGeneratorPage() {
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Angle: <span className="font-mono text-blue-600 dark:text-blue-400">{angle}°</span>
|
||||
<label className="text-sm font-medium text-gray-200">
|
||||
Angle: <span className="font-mono text-blue-600">{angle}°</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -410,7 +410,7 @@ export default function CssGradientGeneratorPage() {
|
|||
max="360"
|
||||
value={angle}
|
||||
onChange={(e) => setAngle(Number(e.target.value) % 361)}
|
||||
className="w-20 px-2 py-1 text-sm font-mono text-right rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-20 px-2 py-1 text-sm font-mono text-right border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
|
|
@ -419,13 +419,13 @@ export default function CssGradientGeneratorPage() {
|
|||
max="360"
|
||||
value={angle}
|
||||
onChange={(e) => setAngle(Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Direction Selector */}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-gray-500">
|
||||
Quick Directions
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
|
|
@ -434,10 +434,10 @@ export default function CssGradientGeneratorPage() {
|
|||
key={dir.angle}
|
||||
type="button"
|
||||
onClick={() => setAngle(dir.angle)}
|
||||
className={`py-2 px-1.5 text-xs font-medium rounded-lg border text-center transition-all ${
|
||||
className={`py-2 px-1.5 text-xs font-medium border text-center transition-all ${
|
||||
angle === dir.angle
|
||||
? "bg-blue-50 dark:bg-blue-900/30 border-blue-500 text-blue-700 dark:text-blue-300 font-semibold"
|
||||
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
? "bg-[#0a0a1a] border-blue-500 text-blue-400 font-semibold"
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-400 hover:bg-black"
|
||||
}`}
|
||||
>
|
||||
{dir.label}
|
||||
|
|
@ -452,17 +452,17 @@ export default function CssGradientGeneratorPage() {
|
|||
{gradientType === "radial" && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<label className="text-sm font-medium text-gray-200">
|
||||
Radial Shape
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRadialShape("circle")}
|
||||
className={`py-2 px-3 text-sm font-medium rounded-lg border transition-all ${
|
||||
className={`py-2 px-3 text-sm font-medium border transition-all ${
|
||||
radialShape === "circle"
|
||||
? "bg-blue-600 border-blue-600 text-white"
|
||||
: "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-400 hover:bg-black"
|
||||
}`}
|
||||
>
|
||||
Circle
|
||||
|
|
@ -470,10 +470,10 @@ export default function CssGradientGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setRadialShape("ellipse")}
|
||||
className={`py-2 px-3 text-sm font-medium rounded-lg border transition-all ${
|
||||
className={`py-2 px-3 text-sm font-medium border transition-all ${
|
||||
radialShape === "ellipse"
|
||||
? "bg-blue-600 border-blue-600 text-white"
|
||||
: "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-400 hover:bg-black"
|
||||
}`}
|
||||
>
|
||||
Ellipse
|
||||
|
|
@ -482,13 +482,13 @@ export default function CssGradientGeneratorPage() {
|
|||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<label className="text-sm font-medium text-gray-200">
|
||||
Radial Position
|
||||
</label>
|
||||
<select
|
||||
value={radialPosition}
|
||||
onChange={(e) => setRadialPosition(e.target.value)}
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
>
|
||||
{RADIAL_POSITIONS.map((pos) => (
|
||||
<option key={pos.value} value={pos.value}>
|
||||
|
|
@ -505,10 +505,10 @@ export default function CssGradientGeneratorPage() {
|
|||
<div className="lg:col-span-7 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="text-sm font-semibold text-gray-200">
|
||||
Color Stops ({colorStops.length})
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-gray-500">
|
||||
Customize start, middle, and end colors with position stops.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -516,7 +516,7 @@ export default function CssGradientGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={reverseColors}
|
||||
className="px-2.5 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors flex items-center gap-1"
|
||||
className="px-2.5 py-1.5 text-xs font-medium text-gray-400 bg-[#111] hover:bg-[#1a1a1a] transition-colors flex items-center gap-1"
|
||||
title="Reverse Color Order"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -533,10 +533,10 @@ export default function CssGradientGeneratorPage() {
|
|||
type="button"
|
||||
onClick={addColorStop}
|
||||
disabled={colorStops.length >= 6}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-lg transition-colors flex items-center gap-1 ${
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-colors flex items-center gap-1 ${
|
||||
colorStops.length >= 6
|
||||
? "bg-gray-200 text-gray-400 cursor-not-allowed dark:bg-gray-800 dark:text-gray-600"
|
||||
: "bg-blue-600 hover:bg-blue-700 text-white shadow-sm"
|
||||
? "bg-[#1a1a1a] text-gray-400 cursor-not-allowed"
|
||||
: "bg-blue-600 hover:bg-blue-700 text-white "
|
||||
}`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -560,12 +560,12 @@ export default function CssGradientGeneratorPage() {
|
|||
return (
|
||||
<div
|
||||
key={stop.id}
|
||||
className="p-3.5 rounded-xl border border-gray-200 dark:border-gray-700/80 bg-gray-50/50 dark:bg-gray-800/50 space-y-2.5 transition-all"
|
||||
className="p-3.5 border border-[#1a1a1a] bg-black/50 space-y-2.5 transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: stop.color }} />
|
||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<span className="w-2.5 h-2.5 rounded-none" style={{ backgroundColor: stop.color }} />
|
||||
<span className="text-xs font-semibold text-gray-400">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -574,7 +574,7 @@ export default function CssGradientGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => removeColorStop(stop.id)}
|
||||
className="text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 transition-colors p-1 rounded"
|
||||
className="text-xs text-red-500 hover:text-red-400 transition-colors p-1 rounded"
|
||||
title="Remove Stop"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -592,7 +592,7 @@ export default function CssGradientGeneratorPage() {
|
|||
<div className="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
|
||||
{/* Color Picker and Hex Input */}
|
||||
<div className="sm:col-span-5 flex items-center gap-2">
|
||||
<div className="relative flex-shrink-0 w-9 h-9 rounded-lg border border-gray-300 dark:border-gray-600 overflow-hidden shadow-inner cursor-pointer">
|
||||
<div className="relative flex-shrink-0 w-9 h-9 border border-[#1a1a1a] overflow-hidden shadow-inner cursor-pointer">
|
||||
<input
|
||||
type="color"
|
||||
value={stop.color}
|
||||
|
|
@ -604,7 +604,7 @@ export default function CssGradientGeneratorPage() {
|
|||
type="text"
|
||||
value={stop.color}
|
||||
onChange={(e) => updateStopColor(stop.id, e.target.value)}
|
||||
className="w-full px-2.5 py-1.5 text-xs font-mono rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none uppercase"
|
||||
className="w-full px-2.5 py-1.5 text-xs font-mono border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none uppercase"
|
||||
placeholder="#000000"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -617,7 +617,7 @@ export default function CssGradientGeneratorPage() {
|
|||
max="100"
|
||||
value={stop.stop}
|
||||
onChange={(e) => updateStopPosition(stop.id, Number(e.target.value))}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-blue-600"
|
||||
className="w-full h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
|
|
@ -626,9 +626,9 @@ export default function CssGradientGeneratorPage() {
|
|||
max="100"
|
||||
value={stop.stop}
|
||||
onChange={(e) => updateStopPosition(stop.id, Number(e.target.value))}
|
||||
className="w-14 px-1.5 py-1 text-xs font-mono text-right rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-14 px-1.5 py-1 text-xs font-mono text-right border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">%</span>
|
||||
<span className="text-xs text-gray-500 font-mono">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -640,20 +640,20 @@ export default function CssGradientGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Generated CSS Output Block */}
|
||||
<div className="space-y-3 pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<div className="space-y-3 pt-4 border-t border-[#1a1a1a]">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-500">
|
||||
Generated CSS
|
||||
</h2>
|
||||
<div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 p-0.5 rounded-lg text-xs">
|
||||
<div className="flex items-center gap-1 bg-[#111] p-0.5 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeFormat("standard")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
codeFormat === "standard"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Standard
|
||||
|
|
@ -661,10 +661,10 @@ export default function CssGradientGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeFormat("full")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
codeFormat === "full"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Full CSS
|
||||
|
|
@ -672,10 +672,10 @@ export default function CssGradientGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setCodeFormat("tailwind")}
|
||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
codeFormat === "tailwind"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-medium shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900"
|
||||
? "bg-[#0a0a0a] text-gray-200 font-medium shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Tailwind
|
||||
|
|
@ -686,7 +686,7 @@ export default function CssGradientGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer"
|
||||
className="inline-flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm transition-colors cursor-pointer"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
|
|
@ -712,7 +712,7 @@ export default function CssGradientGeneratorPage() {
|
|||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<pre className="w-full p-4 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-900 text-gray-100 font-mono text-sm overflow-x-auto leading-relaxed shadow-sm">
|
||||
<pre className="w-full p-4 border border-[#1a1a1a] bg-gray-900 text-gray-100 font-mono text-sm overflow-x-auto leading-relaxed ">
|
||||
<code>{generatedCode}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -330,11 +330,11 @@ export default function CsvToJsonPage() {
|
|||
>
|
||||
<div className="space-y-5">
|
||||
{/* Top Controls Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-4 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={handleConvert}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
|
|
@ -344,7 +344,7 @@ export default function CsvToJsonPage() {
|
|||
|
||||
<button
|
||||
onClick={handleLoadSample}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-50 px-3 py-2 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/50 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#0a0a1a] px-3 py-2 text-sm font-medium text-blue-400 transition-colors hover:bg-[#0a0a1a] cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
|
|
@ -355,24 +355,24 @@ export default function CsvToJsonPage() {
|
|||
<button
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-rose-950/30 dark:hover:text-rose-400 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#111] px-3 py-2 text-sm font-medium text-gray-400 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-400">
|
||||
{/* Delimiter */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor={delimiterSelectId} className="text-xs font-semibold text-gray-500 uppercase dark:text-gray-400">
|
||||
<label htmlFor={delimiterSelectId} className="text-xs font-semibold text-gray-500 uppercase">
|
||||
Delimiter:
|
||||
</label>
|
||||
<select
|
||||
id={delimiterSelectId}
|
||||
value={delimiterSetting}
|
||||
onChange={(e) => setDelimiterSetting(e.target.value)}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value="auto">Auto-detect</option>
|
||||
<option value=",">Comma (,)</option>
|
||||
|
|
@ -384,14 +384,14 @@ export default function CsvToJsonPage() {
|
|||
|
||||
{/* Indent Selector */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor={indentSelectId} className="text-xs font-semibold text-gray-500 uppercase dark:text-gray-400">
|
||||
<label htmlFor={indentSelectId} className="text-xs font-semibold text-gray-500 uppercase">
|
||||
Indent:
|
||||
</label>
|
||||
<select
|
||||
id={indentSelectId}
|
||||
value={indent}
|
||||
onChange={(e) => setIndent(e.target.value)}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value="2">2 Spaces</option>
|
||||
<option value="4">4 Spaces</option>
|
||||
|
|
@ -403,34 +403,34 @@ export default function CsvToJsonPage() {
|
|||
</div>
|
||||
|
||||
{/* Toggles Strip */}
|
||||
<div className="flex flex-wrap items-center gap-5 text-xs text-gray-600 dark:text-gray-400">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<div className="flex flex-wrap items-center gap-5 text-xs text-gray-600">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hasHeaders}
|
||||
onChange={(e) => setHasHeaders(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span className="font-medium">First row as headers (keys)</span>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoParseTypes}
|
||||
onChange={(e) => setAutoParseTypes(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Auto-parse numbers, booleans & null</span>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={unflatten}
|
||||
disabled={!hasHeaders}
|
||||
onChange={(e) => setUnflatten(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 disabled:opacity-40 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900 disabled:opacity-40"
|
||||
/>
|
||||
<span>Unflatten dot keys (e.g. <code>address.city</code> → object)</span>
|
||||
</label>
|
||||
|
|
@ -438,9 +438,9 @@ export default function CsvToJsonPage() {
|
|||
|
||||
{/* Status Notification Banner */}
|
||||
{status.type === "success" && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600 dark:text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
|
|
@ -449,7 +449,7 @@ export default function CsvToJsonPage() {
|
|||
</svg>
|
||||
<span className="font-medium">{status.message}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800">
|
||||
{status.rowCount !== undefined && <span>Rows: <strong>{status.rowCount}</strong></span>}
|
||||
{status.colCount !== undefined && <span>Columns: <strong>{status.colCount}</strong></span>}
|
||||
{status.byteSize !== undefined && <span>Size: <strong>{formatBytes(status.byteSize)}</strong></span>}
|
||||
|
|
@ -458,9 +458,9 @@ export default function CsvToJsonPage() {
|
|||
)}
|
||||
|
||||
{status.type === "error" && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-900 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300">
|
||||
<div className=" border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
|
|
@ -468,8 +468,8 @@ export default function CsvToJsonPage() {
|
|||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-semibold text-red-800 dark:text-red-200">Error Parsing CSV</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-700 dark:text-red-300">{status.message}</p>
|
||||
<p className="font-semibold text-red-400">Error Parsing CSV</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-400">{status.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -481,8 +481,8 @@ export default function CsvToJsonPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Input CSV</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Input CSV</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({inputLineCount} {inputLineCount === 1 ? "line" : "lines"}, {input.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -496,14 +496,14 @@ export default function CsvToJsonPage() {
|
|||
// Permission denied
|
||||
}
|
||||
}}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-[#111] cursor-pointer"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setInput("")}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-red-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-red-400 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-[#111] hover:text-red-400 cursor-pointer"
|
||||
title="Clear input"
|
||||
>
|
||||
Clear
|
||||
|
|
@ -516,7 +516,7 @@ export default function CsvToJsonPage() {
|
|||
placeholder="Paste raw CSV text here (comma, semicolon, or tab separated)..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-white p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -524,8 +524,8 @@ export default function CsvToJsonPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">JSON Output</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">JSON Output</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({outputLineCount} {outputLineCount === 1 ? "line" : "lines"}, {output.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -533,7 +533,7 @@ export default function CsvToJsonPage() {
|
|||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!output}
|
||||
className="inline-flex items-center gap-1 rounded px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:text-gray-300 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium text-gray-400 hover:bg-[#111] disabled:opacity-40 cursor-pointer"
|
||||
title="Download as JSON file"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -544,7 +544,7 @@ export default function CsvToJsonPage() {
|
|||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={!output}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
className="inline-flex items-center gap-1 bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
title="Copy JSON to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
|
|
@ -576,29 +576,29 @@ export default function CsvToJsonPage() {
|
|||
placeholder="Formatted JSON array will appear here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-gray-50 p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-900/80 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-black p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Highlights & Guide */}
|
||||
<div className="mt-8 rounded-xl border border-gray-200 bg-gray-50/70 p-5 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-800/30 dark:text-gray-400">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">About CSV to JSON Converter</h3>
|
||||
<div className="mt-8 border border-[#1a1a1a] bg-black/70 p-5 text-sm text-gray-600">
|
||||
<h3 className="font-semibold text-gray-200">About CSV to JSON Converter</h3>
|
||||
<div className="mt-3 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔍 Smart Delimiter Detection</p>
|
||||
<p className="font-medium text-gray-300">🔍 Smart Delimiter Detection</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Automatically detects whether your data uses commas, tabs, semicolons, or pipes, or choose manually.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🌳 Nested Object Reconstruction</p>
|
||||
<p className="font-medium text-gray-300">🌳 Nested Object Reconstruction</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Reconstitutes dot-notation headers (such as <code>address.city</code>) back into rich nested JSON objects.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🛡️ Full RFC 4180 Support</p>
|
||||
<p className="font-medium text-gray-300">🛡️ Full RFC 4180 Support</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Properly processes escaped quotes, multi-line values, special characters, and numeric types.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -255,12 +255,12 @@ export default function DiffCheckerPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Controls and Stats Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-[#1a1a1a]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSwap}
|
||||
className="px-3.5 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-lg text-xs font-semibold transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
className="px-3.5 py-1.5 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 text-xs font-semibold transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
|
|
@ -270,7 +270,7 @@ export default function DiffCheckerPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleLoadSample}
|
||||
className="px-3.5 py-1.5 bg-blue-50 dark:bg-blue-950/40 hover:bg-blue-100 dark:hover:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
|
||||
className="px-3.5 py-1.5 bg-[#0a0a1a] hover:bg-[#0a0a1a] text-blue-400 text-xs font-semibold transition-colors cursor-pointer"
|
||||
>
|
||||
Load Sample
|
||||
</button>
|
||||
|
|
@ -278,7 +278,7 @@ export default function DiffCheckerPage() {
|
|||
type="button"
|
||||
onClick={handleClear}
|
||||
disabled={!original && !modified}
|
||||
className="px-3.5 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-rose-50 dark:hover:bg-rose-950/30 text-gray-700 dark:text-gray-300 hover:text-rose-600 dark:hover:text-rose-400 disabled:opacity-40 rounded-lg text-xs font-semibold transition-colors cursor-pointer"
|
||||
className="px-3.5 py-1.5 bg-[#111] hover:bg-rose-50 text-gray-400 hover:text-rose-600 disabled:opacity-40 text-xs font-semibold transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -286,34 +286,34 @@ export default function DiffCheckerPage() {
|
|||
|
||||
{/* Options & View mode */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs">
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer text-gray-700 dark:text-gray-300 select-none">
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer text-gray-400 select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={ignoreWhitespace}
|
||||
onChange={(e) => setIgnoreWhitespace(e.target.checked)}
|
||||
className="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-3.5 h-3.5 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Ignore whitespace</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer text-gray-700 dark:text-gray-300 select-none">
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer text-gray-400 select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={ignoreCase}
|
||||
onChange={(e) => setIgnoreCase(e.target.checked)}
|
||||
className="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-3.5 h-3.5 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Ignore case</span>
|
||||
</label>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex items-center rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-100 dark:bg-gray-800 p-0.5">
|
||||
<div className="flex items-center border border-[#1a1a1a] bg-[#111] p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode("unified")}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors cursor-pointer ${
|
||||
className={`px-2.5 py-1 text-xs font-medium transition-colors cursor-pointer ${
|
||||
viewMode === "unified"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Unified
|
||||
|
|
@ -321,10 +321,10 @@ export default function DiffCheckerPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode("split")}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-colors cursor-pointer ${
|
||||
className={`px-2.5 py-1 text-xs font-medium transition-colors cursor-pointer ${
|
||||
viewMode === "split"
|
||||
? "bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Split
|
||||
|
|
@ -339,9 +339,9 @@ export default function DiffCheckerPage() {
|
|||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="diff-original-text"
|
||||
className="text-xs font-bold uppercase tracking-wider text-gray-600 dark:text-gray-300 flex items-center gap-1.5"
|
||||
className="text-xs font-bold uppercase tracking-wider text-gray-600 flex items-center gap-1.5"
|
||||
>
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-rose-500 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-none bg-rose-500 inline-block" />
|
||||
Original Text
|
||||
</label>
|
||||
<span className="text-xs text-gray-400 font-mono">
|
||||
|
|
@ -354,7 +354,7 @@ export default function DiffCheckerPage() {
|
|||
value={original}
|
||||
onChange={(e) => setOriginal(e.target.value)}
|
||||
placeholder="Paste original / before text..."
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -362,9 +362,9 @@ export default function DiffCheckerPage() {
|
|||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="diff-modified-text"
|
||||
className="text-xs font-bold uppercase tracking-wider text-gray-600 dark:text-gray-300 flex items-center gap-1.5"
|
||||
className="text-xs font-bold uppercase tracking-wider text-gray-600 flex items-center gap-1.5"
|
||||
>
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-emerald-500 inline-block" />
|
||||
<span className="w-2.5 h-2.5 rounded-none bg-emerald-500 inline-block" />
|
||||
Modified Text
|
||||
</label>
|
||||
<span className="text-xs text-gray-400 font-mono">
|
||||
|
|
@ -377,45 +377,45 @@ export default function DiffCheckerPage() {
|
|||
value={modified}
|
||||
onChange={(e) => setModified(e.target.value)}
|
||||
placeholder="Paste modified / after text..."
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff Stats Banner */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="rounded-xl border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-emerald-700 dark:text-emerald-400">
|
||||
<div className=" border border-emerald-200 bg-emerald-50/70 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-emerald-700">
|
||||
+ Lines Added
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-emerald-900 dark:text-emerald-200">
|
||||
<p className="mt-1 text-2xl font-extrabold text-emerald-900">
|
||||
+{stats.added}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-rose-200 bg-rose-50/70 dark:border-rose-900/50 dark:bg-rose-950/20 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-rose-700 dark:text-rose-400">
|
||||
<div className=" border border-rose-200 bg-rose-50/70 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-rose-700">
|
||||
- Lines Removed
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-rose-900 dark:text-rose-200">
|
||||
<p className="mt-1 text-2xl font-extrabold text-rose-900">
|
||||
-{stats.removed}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/70 dark:border-gray-800 dark:bg-gray-850 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-400">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-600">
|
||||
Unchanged Lines
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-gray-800 dark:text-gray-200">
|
||||
<p className="mt-1 text-2xl font-extrabold text-gray-300">
|
||||
{stats.unchanged}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-blue-200 bg-blue-50/70 dark:border-blue-900/50 dark:bg-blue-950/20 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-blue-700 dark:text-blue-400">
|
||||
<div className=" border border-blue-900 bg-[#0a0a1a]/70 p-3 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-blue-400">
|
||||
Similarity
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-blue-900 dark:text-blue-200">
|
||||
<p className="mt-1 text-2xl font-extrabold text-blue-900">
|
||||
{stats.similarity}%
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -424,7 +424,7 @@ export default function DiffCheckerPage() {
|
|||
{/* Diff Output Viewer */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<h3 className="text-sm font-bold text-gray-200 flex items-center gap-2">
|
||||
<span>Diff Comparison Output</span>
|
||||
<span className="text-xs font-normal text-gray-500">
|
||||
({viewMode === "unified" ? "Unified View" : "Split View"})
|
||||
|
|
@ -435,45 +435,45 @@ export default function DiffCheckerPage() {
|
|||
type="button"
|
||||
onClick={handleCopyUnifiedDiff}
|
||||
disabled={diffResult.length === 0}
|
||||
className="px-3 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-1 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copiedPatch ? "Copied Patch!" : "Copy Diff Patch"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{diffResult.length === 0 ? (
|
||||
<div className="p-8 text-center rounded-xl border border-dashed border-gray-300 dark:border-gray-700 text-gray-400 text-sm">
|
||||
<div className="p-8 text-center border border-dashed border-[#1a1a1a] text-gray-400 text-sm">
|
||||
Enter original and modified texts above to calculate diff.
|
||||
</div>
|
||||
) : viewMode === "unified" ? (
|
||||
/* Unified Diff View */
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden font-mono text-xs shadow-xs">
|
||||
<div className="max-h-[500px] overflow-y-auto overflow-x-auto divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] overflow-hidden font-mono text-xs shadow-xs">
|
||||
<div className="max-h-[500px] overflow-y-auto overflow-x-auto divide-y divide-gray-100">
|
||||
{diffResult.map((item, idx) => {
|
||||
let bgClass = "bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200";
|
||||
let badge = <span className="text-gray-300 dark:text-gray-600 select-none"> </span>;
|
||||
let bgClass = "bg-[#0a0a0a] text-gray-300";
|
||||
let badge = <span className="text-gray-300 select-none"> </span>;
|
||||
let borderClass = "border-l-4 border-transparent";
|
||||
|
||||
if (item.type === "added") {
|
||||
bgClass = "bg-emerald-50/80 dark:bg-emerald-950/30 text-emerald-950 dark:text-emerald-200";
|
||||
bgClass = "bg-emerald-50/80 text-emerald-950";
|
||||
borderClass = "border-l-4 border-emerald-500";
|
||||
badge = <span className="text-emerald-600 dark:text-emerald-400 font-bold select-none">+</span>;
|
||||
badge = <span className="text-emerald-600 font-bold select-none">+</span>;
|
||||
} else if (item.type === "removed") {
|
||||
bgClass = "bg-rose-50/80 dark:bg-rose-950/30 text-rose-950 dark:text-rose-200";
|
||||
bgClass = "bg-rose-50/80 text-rose-950";
|
||||
borderClass = "border-l-4 border-rose-500";
|
||||
badge = <span className="text-rose-600 dark:text-rose-400 font-bold select-none">-</span>;
|
||||
badge = <span className="text-rose-600 font-bold select-none">-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-stretch hover:bg-black/5 dark:hover:bg-white/5 transition-colors ${bgClass} ${borderClass}`}
|
||||
className={`flex items-stretch hover:bg-black/5 transition-colors ${bgClass} ${borderClass}`}
|
||||
>
|
||||
{/* Line numbers */}
|
||||
<div className="w-10 py-1 pr-2 text-right text-gray-400 dark:text-gray-500 select-none border-r border-gray-200 dark:border-gray-800 shrink-0">
|
||||
<div className="w-10 py-1 pr-2 text-right text-gray-400 select-none border-r border-[#1a1a1a] shrink-0">
|
||||
{item.origLine || ""}
|
||||
</div>
|
||||
<div className="w-10 py-1 pr-2 text-right text-gray-400 dark:text-gray-500 select-none border-r border-gray-200 dark:border-gray-800 shrink-0">
|
||||
<div className="w-10 py-1 pr-2 text-right text-gray-400 select-none border-r border-[#1a1a1a] shrink-0">
|
||||
{item.modLine || ""}
|
||||
</div>
|
||||
{/* Symbol */}
|
||||
|
|
@ -491,25 +491,25 @@ export default function DiffCheckerPage() {
|
|||
</div>
|
||||
) : (
|
||||
/* Split Diff View */
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 overflow-hidden font-mono text-xs shadow-xs">
|
||||
<div className="grid grid-cols-2 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 py-1.5 px-3 text-xs font-semibold text-gray-600 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] overflow-hidden font-mono text-xs shadow-xs">
|
||||
<div className="grid grid-cols-2 bg-[#111] border-b border-[#1a1a1a] py-1.5 px-3 text-xs font-semibold text-gray-600">
|
||||
<div>Original (Before)</div>
|
||||
<div>Modified (After)</div>
|
||||
</div>
|
||||
<div className="max-h-[500px] overflow-y-auto overflow-x-auto divide-y divide-gray-100 dark:divide-gray-800">
|
||||
<div className="max-h-[500px] overflow-y-auto overflow-x-auto divide-y divide-gray-100">
|
||||
{splitRows.map((row, rIdx) => (
|
||||
<div key={rIdx} className="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-800">
|
||||
<div key={rIdx} className="grid grid-cols-2 divide-x divide-[#1a1a1a]">
|
||||
{/* Left Column */}
|
||||
<div
|
||||
className={`flex items-stretch ${
|
||||
row.left
|
||||
? row.left.type === "removed"
|
||||
? "bg-rose-50/80 dark:bg-rose-950/30 text-rose-950 dark:text-rose-200 border-l-4 border-rose-500"
|
||||
: "bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 border-l-4 border-transparent"
|
||||
: "bg-gray-50 dark:bg-gray-950/50"
|
||||
? "bg-rose-50/80 text-rose-950 border-l-4 border-rose-500"
|
||||
: "bg-[#0a0a0a] text-gray-300 border-l-4 border-transparent"
|
||||
: "bg-black"
|
||||
}`}
|
||||
>
|
||||
<div className="w-9 py-1 pr-1.5 text-right text-gray-400 dark:text-gray-500 select-none border-r border-gray-200 dark:border-gray-800 shrink-0">
|
||||
<div className="w-9 py-1 pr-1.5 text-right text-gray-400 select-none border-r border-[#1a1a1a] shrink-0">
|
||||
{row.left?.line || ""}
|
||||
</div>
|
||||
<div className="w-5 py-1 text-center shrink-0 font-bold select-none">
|
||||
|
|
@ -525,12 +525,12 @@ export default function DiffCheckerPage() {
|
|||
className={`flex items-stretch ${
|
||||
row.right
|
||||
? row.right.type === "added"
|
||||
? "bg-emerald-50/80 dark:bg-emerald-950/30 text-emerald-950 dark:text-emerald-200 border-l-4 border-emerald-500"
|
||||
: "bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 border-l-4 border-transparent"
|
||||
: "bg-gray-50 dark:bg-gray-950/50"
|
||||
? "bg-emerald-50/80 text-emerald-950 border-l-4 border-emerald-500"
|
||||
: "bg-[#0a0a0a] text-gray-300 border-l-4 border-transparent"
|
||||
: "bg-black"
|
||||
}`}
|
||||
>
|
||||
<div className="w-9 py-1 pr-1.5 text-right text-gray-400 dark:text-gray-500 select-none border-r border-gray-200 dark:border-gray-800 shrink-0">
|
||||
<div className="w-9 py-1 pr-1.5 text-right text-gray-400 select-none border-r border-[#1a1a1a] shrink-0">
|
||||
{row.right?.line || ""}
|
||||
</div>
|
||||
<div className="w-5 py-1 text-center shrink-0 font-bold select-none">
|
||||
|
|
|
|||
|
|
@ -286,11 +286,11 @@ export default function HashGeneratorPage() {
|
|||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<label
|
||||
htmlFor="hash-input"
|
||||
className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
className="text-xs font-semibold uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Input Text
|
||||
</label>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||
<span>{inputText.length} characters</span>
|
||||
<span>•</span>
|
||||
<span>{byteLength} UTF-8 bytes</span>
|
||||
|
|
@ -303,19 +303,19 @@ export default function HashGeneratorPage() {
|
|||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
placeholder="Type or paste text to generate cryptographic hashes..."
|
||||
className="w-full rounded-lg border border-gray-300 bg-white p-3 font-mono text-sm text-gray-900 shadow-inner focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-sm text-gray-200 shadow-inner focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
|
||||
{/* Quick Presets and Clear */}
|
||||
<div className="mt-2.5 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Samples:</span>
|
||||
<span className="text-xs text-gray-500">Samples:</span>
|
||||
{sampleTexts.map((sample) => (
|
||||
<button
|
||||
key={sample.label}
|
||||
type="button"
|
||||
onClick={() => setInputText(sample.text)}
|
||||
className="rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className=" bg-[#111] px-2 py-1 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a]"
|
||||
>
|
||||
{sample.label}
|
||||
</button>
|
||||
|
|
@ -326,7 +326,7 @@ export default function HashGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setInputText("")}
|
||||
className="text-xs font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
className="text-xs font-medium text-red-400 hover:text-red-400"
|
||||
>
|
||||
Clear input
|
||||
</button>
|
||||
|
|
@ -335,18 +335,18 @@ export default function HashGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Hashes Section Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-3 dark:border-gray-700">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-gray-400">
|
||||
Computed Hashes
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-xs font-medium text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={uppercase}
|
||||
onChange={(e) => setUppercase(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>UPPERCASE HEX</span>
|
||||
</label>
|
||||
|
|
@ -354,10 +354,10 @@ export default function HashGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={copyAllHashes}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
copiedAll
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
: "bg-[#111] text-gray-400 hover:bg-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
{copiedAll ? (
|
||||
|
|
@ -393,25 +393,25 @@ export default function HashGeneratorPage() {
|
|||
return (
|
||||
<div
|
||||
key={item.algorithm}
|
||||
className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 transition-all hover:border-gray-300 dark:border-gray-700/80 dark:bg-gray-800/40 dark:hover:border-gray-600"
|
||||
className=" border border-[#1a1a1a] bg-black/50 p-4 transition-all hover:border-[#1a1a1a]"
|
||||
>
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-gray-900 dark:text-white">
|
||||
<span className="font-bold text-gray-200">
|
||||
{item.algorithm}
|
||||
</span>
|
||||
<span className="rounded bg-gray-200 px-1.5 py-0.5 text-[10px] font-semibold text-gray-700 dark:bg-gray-700 dark:text-gray-300">
|
||||
<span className="rounded bg-[#1a1a1a] px-1.5 py-0.5 text-[10px] font-semibold text-gray-400">
|
||||
{item.bitLength}-bit ({item.hash.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500">
|
||||
{item.securityNote}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1 overflow-hidden rounded-md border border-gray-300 bg-white p-2.5 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p className="select-all break-all font-mono text-xs font-medium text-gray-800 dark:text-gray-200 sm:text-sm">
|
||||
<div className="relative flex-1 overflow-hidden border border-[#1a1a1a] bg-[#0a0a0a] p-2.5">
|
||||
<p className="select-all break-all font-mono text-xs font-medium text-gray-300 sm:text-sm">
|
||||
{displayHash || "—"}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -420,7 +420,7 @@ export default function HashGeneratorPage() {
|
|||
type="button"
|
||||
onClick={() => copyHash(item.hash, item.algorithm)}
|
||||
aria-label={`Copy ${item.algorithm} hash`}
|
||||
className={`flex shrink-0 items-center gap-1.5 rounded-md px-3 py-2 text-xs font-medium transition-all ${
|
||||
className={`flex shrink-0 items-center gap-1.5 px-3 py-2 text-xs font-medium transition-all ${
|
||||
isCopied
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-blue-600 text-white hover:bg-blue-700 active:scale-95"
|
||||
|
|
@ -454,8 +454,8 @@ export default function HashGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Educational / Comparison Guide */}
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50/80 p-4 text-xs text-gray-500 dark:border-gray-800 dark:bg-gray-800/40 dark:text-gray-400">
|
||||
<h4 className="mb-2 font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div className=" border border-gray-100 bg-black/80 p-4 text-xs text-gray-500">
|
||||
<h4 className="mb-2 font-semibold text-gray-300">
|
||||
About Cryptographic Hashes
|
||||
</h4>
|
||||
<p className="leading-relaxed">
|
||||
|
|
|
|||
|
|
@ -188,12 +188,12 @@ export default function HtmlEntitiesPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Controls Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-[#1a1a1a]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEncode}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
|
|
@ -203,7 +203,7 @@ export default function HtmlEntitiesPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleDecode}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
|
|
@ -214,7 +214,7 @@ export default function HtmlEntitiesPage() {
|
|||
type="button"
|
||||
onClick={handleSwap}
|
||||
disabled={!output && !input}
|
||||
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-200 rounded-lg text-sm font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-2 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-sm font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
title="Swap input and output"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -226,7 +226,7 @@ export default function HtmlEntitiesPage() {
|
|||
type="button"
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-950/30 text-gray-700 dark:text-gray-300 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed rounded-lg text-sm font-medium transition-colors cursor-pointer"
|
||||
className="px-3 py-2 bg-[#111] hover:bg-[#1a0a0a] text-gray-400 hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed text-sm font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -234,25 +234,25 @@ export default function HtmlEntitiesPage() {
|
|||
|
||||
{/* Sample buttons */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400 font-medium">Samples:</span>
|
||||
<span className="text-gray-500 font-medium">Samples:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("html")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
HTML Code
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("symbols")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Symbols & Quotes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("entities")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Encoded Entities
|
||||
</button>
|
||||
|
|
@ -260,8 +260,8 @@ export default function HtmlEntitiesPage() {
|
|||
</div>
|
||||
|
||||
{/* Encoding Mode Selector */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800/50 p-3 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<span className="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-400 bg-black p-3 border border-[#1a1a1a]">
|
||||
<span className="font-medium text-xs text-gray-500 uppercase tracking-wider">
|
||||
Encoding Format:
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
|
|
@ -277,9 +277,9 @@ export default function HtmlEntitiesPage() {
|
|||
encodeText(input, "basic");
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Basic Special Chars <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(< > & " ')</code></span>
|
||||
<span>Basic Special Chars <code className="text-xs bg-[#1a1a1a] px-1 py-0.5 font-mono">(< > & " ')</code></span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
|
|
@ -293,9 +293,9 @@ export default function HtmlEntitiesPage() {
|
|||
encodeText(input, "extended");
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Extended Named <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(&copy;, &euro;, etc.)</code></span>
|
||||
<span>Extended Named <code className="text-xs bg-[#1a1a1a] px-1 py-0.5 font-mono">(&copy;, &euro;, etc.)</code></span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
|
|
@ -309,9 +309,9 @@ export default function HtmlEntitiesPage() {
|
|||
encodeText(input, "decimal");
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Decimal Numeric <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(&#60;)</code></span>
|
||||
<span>Decimal Numeric <code className="text-xs bg-[#1a1a1a] px-1 py-0.5 font-mono">(&#60;)</code></span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
|
|
@ -325,16 +325,16 @@ export default function HtmlEntitiesPage() {
|
|||
encodeText(input, "hex");
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Hexadecimal <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">(&#x3C;)</code></span>
|
||||
<span>Hexadecimal <code className="text-xs bg-[#1a1a1a] px-1 py-0.5 font-mono">(&#x3C;)</code></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Notification */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-800 rounded-lg text-red-800 dark:text-red-300 text-sm">
|
||||
<div className="flex items-start gap-3 p-4 bg-[#1a0a0a] border border-red-900 text-red-400 text-sm">
|
||||
<svg className="w-5 h-5 text-red-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
|
@ -350,10 +350,10 @@ export default function HtmlEntitiesPage() {
|
|||
{/* Input Area */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={inputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={inputId} className="font-medium text-sm text-gray-400">
|
||||
Input Text / HTML
|
||||
</label>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{input.length} chars
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -366,7 +366,7 @@ export default function HtmlEntitiesPage() {
|
|||
}}
|
||||
placeholder="Type or paste HTML code or text to encode/decode..."
|
||||
rows={12}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed shadow-inner"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y placeholder:text-gray-400 leading-relaxed shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -374,31 +374,31 @@ export default function HtmlEntitiesPage() {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor={outputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={outputId} className="font-medium text-sm text-gray-400">
|
||||
Output Result
|
||||
</label>
|
||||
{lastAction && (
|
||||
<span className="px-2 py-0.5 text-xs rounded-full font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300">
|
||||
<span className="px-2 py-0.5 text-xs rounded-none font-medium bg-[#0a0a1a] text-blue-400">
|
||||
{lastAction === "encode" ? "HTML Entities Encoded" : "Decoded Plain Text"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{output.length} chars
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
disabled={!output}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-green-600 dark:text-green-400 font-semibold">Copied!</span>
|
||||
<span className="text-green-400 font-semibold">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -417,38 +417,38 @@ export default function HtmlEntitiesPage() {
|
|||
readOnly
|
||||
placeholder="Output will appear here..."
|
||||
rows={12}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/60 text-gray-900 dark:text-gray-100 font-mono text-sm focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed select-all"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-black text-gray-200 font-mono text-sm focus:outline-none resize-y placeholder:text-gray-400 leading-relaxed select-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* HTML Entities Reference Table */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">Key HTML Entities Reference</h3>
|
||||
<div className="mt-8 pt-6 border-t border-[#1a1a1a]">
|
||||
<h3 className="text-sm font-semibold text-gray-200 mb-3">Key HTML Entities Reference</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2 text-xs font-mono">
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 font-sans">< (Less than)</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">&lt;</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between items-center">
|
||||
<span className="text-gray-600 font-sans">< (Less than)</span>
|
||||
<span className="font-semibold text-blue-600">&lt;</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 font-sans">> (Greater)</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">&gt;</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between items-center">
|
||||
<span className="text-gray-600 font-sans">> (Greater)</span>
|
||||
<span className="font-semibold text-blue-600">&gt;</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 font-sans">& (Ampersand)</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">&amp;</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between items-center">
|
||||
<span className="text-gray-600 font-sans">& (Ampersand)</span>
|
||||
<span className="font-semibold text-blue-600">&amp;</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 font-sans">" (Double quote)</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">&quot;</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between items-center">
|
||||
<span className="text-gray-600 font-sans">" (Double quote)</span>
|
||||
<span className="font-semibold text-blue-600">&quot;</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 font-sans">' (Single quote)</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">&#39;</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between items-center">
|
||||
<span className="text-gray-600 font-sans">' (Single quote)</span>
|
||||
<span className="font-semibold text-blue-600">&#39;</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<span className="text-gray-600 dark:text-gray-400 font-sans">© (Copyright)</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">&copy;</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between items-center">
|
||||
<span className="text-gray-600 font-sans">© (Copyright)</span>
|
||||
<span className="font-semibold text-blue-600">&copy;</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -258,12 +258,12 @@ export default function ImageToBase64Page() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Top Action / Samples Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-3 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="flex cursor-pointer items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 active:bg-blue-800"
|
||||
className="flex cursor-pointer items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l4-4m0 0l4 4m-4-4v12" />
|
||||
|
|
@ -275,7 +275,7 @@ export default function ImageToBase64Page() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="flex cursor-pointer items-center gap-1 rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-red-50 hover:text-red-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-red-950/40 dark:hover:text-red-400"
|
||||
className="flex cursor-pointer items-center gap-1 bg-[#111] px-3 py-2 text-sm font-medium text-gray-400 transition-colors hover:bg-[#1a0a0a] hover:text-red-400"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
|
|
@ -286,18 +286,18 @@ export default function ImageToBase64Page() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="font-medium text-gray-500 dark:text-gray-400">Try Sample:</span>
|
||||
<span className="font-medium text-gray-500">Try Sample:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSampleImage("badge")}
|
||||
className="cursor-pointer rounded bg-gray-100 px-2.5 py-1 text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="cursor-pointer bg-[#111] px-2.5 py-1 text-gray-400 transition-colors hover:bg-[#1a1a1a]"
|
||||
>
|
||||
Badge (PNG)
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSampleImage("icon")}
|
||||
className="cursor-pointer rounded bg-gray-100 px-2.5 py-1 text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="cursor-pointer bg-[#111] px-2.5 py-1 text-gray-400 transition-colors hover:bg-[#1a1a1a]"
|
||||
>
|
||||
Vector Icon (SVG)
|
||||
</button>
|
||||
|
|
@ -315,7 +315,7 @@ export default function ImageToBase64Page() {
|
|||
|
||||
{/* Error notification */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-800 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300">
|
||||
<div className="flex items-start gap-3 border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-400">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
|
@ -333,47 +333,47 @@ export default function ImageToBase64Page() {
|
|||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`flex cursor-pointer flex-col items-center justify-center rounded-xl border-2 border-dashed p-10 text-center transition-all ${
|
||||
className={`flex cursor-pointer flex-col items-center justify-center border-2 border-dashed p-10 text-center transition-all ${
|
||||
isDragging
|
||||
? "border-blue-500 bg-blue-50/60 dark:border-blue-400 dark:bg-blue-950/30 scale-[1.01]"
|
||||
: "border-gray-300 bg-white hover:border-blue-400 hover:bg-gray-50/50 dark:border-gray-700 dark:bg-gray-800/40 dark:hover:border-gray-600 dark:hover:bg-gray-800/80"
|
||||
? "border-blue-500 bg-[#0a0a1a]/60 scale-[1.01]"
|
||||
: "border-[#1a1a1a] bg-[#0a0a0a] hover:border-blue-400 hover:bg-black/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-blue-50 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400 mb-4">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-none bg-[#0a0a1a] text-blue-600 mb-4">
|
||||
<svg className="h-8 w-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<p className="text-base font-semibold text-gray-800 dark:text-gray-200">
|
||||
Drag & drop your image here, or <span className="text-blue-600 dark:text-blue-400 underline">browse files</span>
|
||||
<p className="text-base font-semibold text-gray-300">
|
||||
Drag & drop your image here, or <span className="text-blue-600 underline">browse files</span>
|
||||
</p>
|
||||
<p className="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="mt-1.5 text-xs text-gray-500">
|
||||
Supports PNG, JPG, JPEG, SVG, WebP, GIF, ICO, AVIF, BMP • Paste directly with Ctrl+V / ⌘V
|
||||
</p>
|
||||
<div className="mt-4 flex items-center gap-2 text-xs text-gray-400">
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-green-500"></span>
|
||||
<span className="inline-block h-2 w-2 rounded-none bg-[#0a1a0a]0"></span>
|
||||
<span>100% Client-side processing — No files uploaded to servers</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* Image Preview & Details Card */}
|
||||
<div className="grid grid-cols-1 gap-6 rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800 lg:grid-cols-12">
|
||||
<div className="grid grid-cols-1 gap-6 border border-[#1a1a1a] bg-[#0a0a0a] p-5 lg:grid-cols-12">
|
||||
{/* Preview Box */}
|
||||
<div className="flex flex-col items-center justify-center lg:col-span-4">
|
||||
<div className="relative flex max-h-56 min-h-[160px] w-full items-center justify-center overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:12px_12px] dark:bg-[radial-gradient(#374151_1px,transparent_1px)] p-2">
|
||||
<div className="relative flex max-h-56 min-h-[160px] w-full items-center justify-center overflow-hidden border border-[#1a1a1a] bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:12px_12px](#374151_1px,transparent_1px)] p-2">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={image.dataUri}
|
||||
alt={image.name}
|
||||
className="max-h-48 max-w-full rounded object-contain shadow-sm"
|
||||
className="max-h-48 max-w-full object-contain "
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="mt-3 text-xs text-blue-600 hover:underline dark:text-blue-400 font-medium cursor-pointer"
|
||||
className="mt-3 text-xs text-blue-600 hover:underline font-medium cursor-pointer"
|
||||
>
|
||||
Change Image
|
||||
</button>
|
||||
|
|
@ -382,41 +382,41 @@ export default function ImageToBase64Page() {
|
|||
{/* Metadata & Quick Actions */}
|
||||
<div className="flex flex-col justify-between space-y-4 lg:col-span-8">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-gray-100 pb-3 dark:border-gray-700">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white truncate max-w-xs sm:max-w-md" title={image.name}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-gray-100 pb-3">
|
||||
<h3 className="font-semibold text-gray-200 truncate max-w-xs sm:max-w-md" title={image.name}>
|
||||
{image.name}
|
||||
</h3>
|
||||
<span className="rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-semibold text-blue-800 dark:bg-blue-900/60 dark:text-blue-300">
|
||||
<span className="rounded-none bg-[#0a0a1a] px-2.5 py-0.5 text-xs font-semibold text-blue-400">
|
||||
{image.type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Details Grid */}
|
||||
<div className="mt-3 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div className="rounded-lg bg-gray-50 p-2.5 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<span className="block text-[11px] font-medium text-gray-500 dark:text-gray-400 uppercase">Dimensions</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div className=" bg-black p-2.5 border border-[#1a1a1a]">
|
||||
<span className="block text-[11px] font-medium text-gray-500 uppercase">Dimensions</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-300">
|
||||
{image.width > 0 ? `${image.width} × ${image.height} px` : "Vector (SVG)"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-gray-50 p-2.5 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<span className="block text-[11px] font-medium text-gray-500 dark:text-gray-400 uppercase">Original Size</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div className=" bg-black p-2.5 border border-[#1a1a1a]">
|
||||
<span className="block text-[11px] font-medium text-gray-500 uppercase">Original Size</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-300">
|
||||
{formatBytes(image.size)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-gray-50 p-2.5 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<span className="block text-[11px] font-medium text-gray-500 dark:text-gray-400 uppercase">Base64 Length</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div className=" bg-black p-2.5 border border-[#1a1a1a]">
|
||||
<span className="block text-[11px] font-medium text-gray-500 uppercase">Base64 Length</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-300">
|
||||
{image.dataUri.length.toLocaleString()} chars
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-gray-50 p-2.5 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<span className="block text-[11px] font-medium text-gray-500 dark:text-gray-400 uppercase">Base64 Size</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div className=" bg-black p-2.5 border border-[#1a1a1a]">
|
||||
<span className="block text-[11px] font-medium text-gray-500 uppercase">Base64 Size</span>
|
||||
<span className="font-mono text-sm font-semibold text-gray-300">
|
||||
{formatBytes(base64Bytes)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -424,14 +424,14 @@ export default function ImageToBase64Page() {
|
|||
</div>
|
||||
|
||||
{/* Size Comparison Bar */}
|
||||
<div className="rounded-lg bg-gray-50 p-3 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between text-xs mb-1.5 font-medium text-gray-700 dark:text-gray-300">
|
||||
<div className=" bg-black p-3 border border-[#1a1a1a]">
|
||||
<div className="flex items-center justify-between text-xs mb-1.5 font-medium text-gray-400">
|
||||
<span>Size Overhead Comparison</span>
|
||||
<span className="font-mono text-amber-600 dark:text-amber-400">
|
||||
<span className="font-mono text-amber-600">
|
||||
+{sizeIncreasePercent}% (+{formatBytes(sizeDiffBytes)})
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700 flex">
|
||||
<div className="h-2.5 w-full overflow-hidden rounded-none bg-[#1a1a1a] flex">
|
||||
<div
|
||||
className="bg-blue-600 h-full"
|
||||
style={{ width: `${Math.min(100, (originalBytes / base64Bytes) * 100)}%` }}
|
||||
|
|
@ -443,13 +443,13 @@ export default function ImageToBase64Page() {
|
|||
title={`Base64 Encoding Overhead: +${formatBytes(sizeDiffBytes)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-[11px] text-gray-500 dark:text-gray-400 mt-1.5">
|
||||
<div className="flex items-center justify-between text-[11px] text-gray-500 mt-1.5">
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-blue-600 inline-block"></span>
|
||||
<span className="h-2 w-2 rounded-none bg-blue-600 inline-block"></span>
|
||||
Original ({formatBytes(originalBytes)})
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="h-2 w-2 rounded-full bg-amber-500 inline-block"></span>
|
||||
<span className="h-2 w-2 rounded-none bg-amber-500 inline-block"></span>
|
||||
Base64 Overhead (~33% ASCII expansion)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -461,14 +461,14 @@ export default function ImageToBase64Page() {
|
|||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
{/* Tabs */}
|
||||
<div className="inline-flex rounded-lg border border-gray-200 bg-gray-100 p-1 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="inline-flex border border-[#1a1a1a] bg-[#111] p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("data-uri")}
|
||||
className={`cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
className={`cursor-pointer px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
activeTab === "data-uri"
|
||||
? "bg-white text-blue-600 shadow-sm dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 "
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Data URI
|
||||
|
|
@ -476,10 +476,10 @@ export default function ImageToBase64Page() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("raw")}
|
||||
className={`cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
className={`cursor-pointer px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
activeTab === "raw"
|
||||
? "bg-white text-blue-600 shadow-sm dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 "
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Raw Base64
|
||||
|
|
@ -487,10 +487,10 @@ export default function ImageToBase64Page() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("html")}
|
||||
className={`cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
className={`cursor-pointer px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
activeTab === "html"
|
||||
? "bg-white text-blue-600 shadow-sm dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 "
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
HTML <img>
|
||||
|
|
@ -498,10 +498,10 @@ export default function ImageToBase64Page() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("css")}
|
||||
className={`cursor-pointer rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
className={`cursor-pointer px-3 py-1.5 text-xs font-semibold transition-colors ${
|
||||
activeTab === "css"
|
||||
? "bg-white text-blue-600 shadow-sm dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 "
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
CSS Background
|
||||
|
|
@ -512,7 +512,7 @@ export default function ImageToBase64Page() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(getOutputText(activeTab), `active-${activeTab}`)}
|
||||
className="flex cursor-pointer items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 active:bg-blue-800"
|
||||
className="flex cursor-pointer items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
{copiedKey === `active-${activeTab}` ? (
|
||||
<>
|
||||
|
|
@ -539,60 +539,60 @@ export default function ImageToBase64Page() {
|
|||
rows={8}
|
||||
value={getOutputText(activeTab)}
|
||||
placeholder="Base64 output will appear here..."
|
||||
className="w-full rounded-lg border border-gray-300 bg-white p-3.5 font-mono text-xs text-gray-900 shadow-inner focus:outline-none dark:border-gray-700 dark:bg-gray-900/80 dark:text-gray-100 leading-relaxed select-all"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] p-3.5 font-mono text-xs text-gray-200 shadow-inner focus:outline-none leading-relaxed select-all"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Copy Cards for All 4 Formats */}
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 pt-2">
|
||||
{/* 1. Data URI */}
|
||||
<div className="flex flex-col justify-between rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-800 dark:text-gray-200">Full Data URI</span>
|
||||
<span className="text-xs font-semibold text-gray-300">Full Data URI</span>
|
||||
<span className="text-[10px] text-gray-400">data:image/...</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400 line-clamp-2 font-mono">
|
||||
<p className="mt-1 text-[11px] text-gray-500 line-clamp-2 font-mono">
|
||||
{image.dataUri}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(image.dataUri, "quick-data-uri")}
|
||||
className="mt-2.5 w-full cursor-pointer rounded bg-gray-100 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="mt-2.5 w-full cursor-pointer bg-[#111] py-1.5 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "quick-data-uri" ? "✓ Copied!" : "Copy Data URI"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 2. Raw Base64 */}
|
||||
<div className="flex flex-col justify-between rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-800 dark:text-gray-200">Raw Base64</span>
|
||||
<span className="text-xs font-semibold text-gray-300">Raw Base64</span>
|
||||
<span className="text-[10px] text-gray-400">No header</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400 line-clamp-2 font-mono">
|
||||
<p className="mt-1 text-[11px] text-gray-500 line-clamp-2 font-mono">
|
||||
{image.rawBase64}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(image.rawBase64, "quick-raw")}
|
||||
className="mt-2.5 w-full cursor-pointer rounded bg-gray-100 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="mt-2.5 w-full cursor-pointer bg-[#111] py-1.5 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "quick-raw" ? "✓ Copied!" : "Copy Raw Base64"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 3. HTML <img> */}
|
||||
<div className="flex flex-col justify-between rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-800 dark:text-gray-200">HTML <img></span>
|
||||
<span className="text-xs font-semibold text-gray-300">HTML <img></span>
|
||||
<span className="text-[10px] text-gray-400"><img src=...></span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400 line-clamp-2 font-mono">
|
||||
<p className="mt-1 text-[11px] text-gray-500 line-clamp-2 font-mono">
|
||||
{`<img src="${image.dataUri}" alt="${image.name}" />`}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -604,20 +604,20 @@ export default function ImageToBase64Page() {
|
|||
"quick-html"
|
||||
)
|
||||
}
|
||||
className="mt-2.5 w-full cursor-pointer rounded bg-gray-100 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="mt-2.5 w-full cursor-pointer bg-[#111] py-1.5 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "quick-html" ? "✓ Copied!" : "Copy HTML Tag"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 4. CSS background-image */}
|
||||
<div className="flex flex-col justify-between rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-3">
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-800 dark:text-gray-200">CSS Background</span>
|
||||
<span className="text-xs font-semibold text-gray-300">CSS Background</span>
|
||||
<span className="text-[10px] text-gray-400">background-image</span>
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400 line-clamp-2 font-mono">
|
||||
<p className="mt-1 text-[11px] text-gray-500 line-clamp-2 font-mono">
|
||||
{`background-image: url("${image.dataUri}");`}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -626,7 +626,7 @@ export default function ImageToBase64Page() {
|
|||
onClick={() =>
|
||||
copyToClipboard(`background-image: url("${image.dataUri}");`, "quick-css")
|
||||
}
|
||||
className="mt-2.5 w-full cursor-pointer rounded bg-gray-100 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="mt-2.5 w-full cursor-pointer bg-[#111] py-1.5 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "quick-css" ? "✓ Copied!" : "Copy CSS"}
|
||||
</button>
|
||||
|
|
@ -637,21 +637,21 @@ export default function ImageToBase64Page() {
|
|||
)}
|
||||
|
||||
{/* Informational Cards */}
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 border-t border-gray-200 pt-6 text-xs text-gray-600 dark:border-gray-800 dark:text-gray-400 md:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">When to Use Base64 Images</h4>
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 border-t border-[#1a1a1a] pt-6 text-xs text-gray-600 md:grid-cols-3">
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">When to Use Base64 Images</h4>
|
||||
<p>
|
||||
Ideal for embedding small icons (<10KB), logos in standalone HTML/emails, offline web apps, and preventing render-blocking HTTP requests.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">Understanding the ~33% Overhead</h4>
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">Understanding the ~33% Overhead</h4>
|
||||
<p>
|
||||
Base64 encodes every 3 binary bytes into 4 ASCII characters (a 33.3% size expansion). For large photographs, standard image hosting with CDN caching is recommended.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">Private & In-Browser Execution</h4>
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">Private & In-Browser Execution</h4>
|
||||
<p>
|
||||
Your graphics never touch an external server or API. All binary-to-string transformation executes natively via HTML5 FileReader in your browser.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -302,11 +302,11 @@ export default function JsonFormatterPage() {
|
|||
>
|
||||
<div className="space-y-5">
|
||||
{/* Controls Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-4 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={handleFormat}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95"
|
||||
className="inline-flex items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
|
|
@ -316,7 +316,7 @@ export default function JsonFormatterPage() {
|
|||
|
||||
<button
|
||||
onClick={handleMinify}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-800 transition-colors hover:bg-gray-300 active:scale-95 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
className="inline-flex items-center gap-1.5 bg-[#1a1a1a] px-4 py-2 text-sm font-medium text-gray-300 transition-colors hover:bg-[#222] active:scale-95"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
|
|
@ -326,7 +326,7 @@ export default function JsonFormatterPage() {
|
|||
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700 active:scale-95 dark:bg-emerald-700 dark:hover:bg-emerald-600"
|
||||
className="inline-flex items-center gap-1.5 bg-emerald-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700 active:scale-95"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
|
|
@ -337,7 +337,7 @@ export default function JsonFormatterPage() {
|
|||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Indent Selector */}
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-center gap-1.5 text-sm text-gray-600">
|
||||
<label htmlFor={indentSelectId} className="text-xs font-medium uppercase tracking-wide">
|
||||
Indent:
|
||||
</label>
|
||||
|
|
@ -345,7 +345,7 @@ export default function JsonFormatterPage() {
|
|||
id={indentSelectId}
|
||||
value={indent}
|
||||
onChange={(e) => setIndent(e.target.value)}
|
||||
className="rounded-md border border-gray-300 bg-white px-2.5 py-1 text-sm text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1 text-sm text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value="2">2 Spaces</option>
|
||||
<option value="4">4 Spaces</option>
|
||||
|
|
@ -355,26 +355,26 @@ export default function JsonFormatterPage() {
|
|||
</div>
|
||||
|
||||
{/* Sort Keys Toggle */}
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-sm text-gray-700 select-none dark:text-gray-300">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-sm text-gray-400 select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sortKeys}
|
||||
onChange={(e) => setSortKeys(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Sort Keys</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
onClick={handleLoadSample}
|
||||
className="rounded-md px-2.5 py-1 text-xs font-medium text-gray-600 underline-offset-2 hover:text-blue-600 hover:underline dark:text-gray-400 dark:hover:text-blue-400"
|
||||
className=" px-2.5 py-1 text-xs font-medium text-gray-600 underline-offset-2 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
Load Sample
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="rounded-md px-2.5 py-1 text-xs font-medium text-red-600 underline-offset-2 hover:text-red-700 hover:underline dark:text-red-400 dark:hover:text-red-300"
|
||||
className=" px-2.5 py-1 text-xs font-medium text-red-400 underline-offset-2 hover:text-red-400 hover:underline"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -383,9 +383,9 @@ export default function JsonFormatterPage() {
|
|||
|
||||
{/* Status Notification Banner */}
|
||||
{status.type === "success" && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600 dark:text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
|
|
@ -394,7 +394,7 @@ export default function JsonFormatterPage() {
|
|||
</svg>
|
||||
<span className="font-medium">{status.message}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800">
|
||||
{status.byteSize !== undefined && <span>Size: <strong>{formatBytes(status.byteSize)}</strong></span>}
|
||||
{status.itemCount !== undefined && <span>Keys: <strong>{status.itemCount}</strong></span>}
|
||||
{status.depth !== undefined && <span>Depth: <strong>{status.depth}</strong></span>}
|
||||
|
|
@ -403,9 +403,9 @@ export default function JsonFormatterPage() {
|
|||
)}
|
||||
|
||||
{status.type === "error" && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-900 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300">
|
||||
<div className=" border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
|
|
@ -413,10 +413,10 @@ export default function JsonFormatterPage() {
|
|||
/>
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-red-800 dark:text-red-200">Invalid JSON Syntax</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-700 dark:text-red-300">{status.message}</p>
|
||||
<p className="font-semibold text-red-400">Invalid JSON Syntax</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-400">{status.message}</p>
|
||||
{(status.line !== undefined || status.column !== undefined) && (
|
||||
<div className="mt-2 inline-flex items-center gap-2 rounded bg-red-100 px-2.5 py-1 text-xs font-medium text-red-800 dark:bg-red-900/60 dark:text-red-200">
|
||||
<div className="mt-2 inline-flex items-center gap-2 bg-[#1a0a0a] px-2.5 py-1 text-xs font-medium text-red-400">
|
||||
<span>📍 Error location:</span>
|
||||
{status.line !== undefined && <span>Line <strong>{status.line}</strong></span>}
|
||||
{status.column !== undefined && <span>Column <strong>{status.column}</strong></span>}
|
||||
|
|
@ -433,8 +433,8 @@ export default function JsonFormatterPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Input JSON</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Input JSON</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({inputLineCount} {inputLineCount === 1 ? "line" : "lines"}, {input.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -448,14 +448,14 @@ export default function JsonFormatterPage() {
|
|||
// Clipboard permission denied or unavailable
|
||||
}
|
||||
}}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-[#111]"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setInput("")}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-red-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-red-400"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-[#111] hover:text-red-400"
|
||||
title="Clear input"
|
||||
>
|
||||
Clear
|
||||
|
|
@ -468,7 +468,7 @@ export default function JsonFormatterPage() {
|
|||
placeholder="Paste or type raw JSON here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-white p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -476,8 +476,8 @@ export default function JsonFormatterPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Output Result</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Output Result</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({outputLineCount} {outputLineCount === 1 ? "line" : "lines"}, {output.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -485,7 +485,7 @@ export default function JsonFormatterPage() {
|
|||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!output && !input}
|
||||
className="inline-flex items-center gap-1 rounded px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium text-gray-400 hover:bg-[#111] disabled:opacity-40"
|
||||
title="Download as JSON file"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -496,7 +496,7 @@ export default function JsonFormatterPage() {
|
|||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={!output && !input}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40"
|
||||
className="inline-flex items-center gap-1 bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
|
|
@ -528,29 +528,29 @@ export default function JsonFormatterPage() {
|
|||
placeholder="Formatted or minified output will appear here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-gray-50 p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-900/80 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-black p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Highlights & Guide */}
|
||||
<div className="mt-8 rounded-xl border border-gray-200 bg-gray-50/70 p-5 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-800/30 dark:text-gray-400">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">About JSON Formatter & Validator</h3>
|
||||
<div className="mt-8 border border-[#1a1a1a] bg-black/70 p-5 text-sm text-gray-600">
|
||||
<h3 className="font-semibold text-gray-200">About JSON Formatter & Validator</h3>
|
||||
<div className="mt-3 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">✨ Beautify & Format</p>
|
||||
<p className="font-medium text-gray-300">✨ Beautify & Format</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Format unorganized or compact JSON with customizable 2-space, 4-space, or tab indentation for maximum readability.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔍 Strict Validation</p>
|
||||
<p className="font-medium text-gray-300">🔍 Strict Validation</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Identify syntax mistakes, unquoted keys, trailing commas, and locate exact line numbers and column coordinates instantly.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔒 100% Client-Side</p>
|
||||
<p className="font-medium text-gray-300">🔒 100% Client-Side</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Your data never leaves your browser. Parsing and formatting run entirely on your local machine for complete privacy.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -344,11 +344,11 @@ export default function JsonToCsvPage() {
|
|||
>
|
||||
<div className="space-y-5">
|
||||
{/* Top Controls Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-4 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={handleConvert}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
|
|
@ -358,7 +358,7 @@ export default function JsonToCsvPage() {
|
|||
|
||||
<button
|
||||
onClick={handleLoadSample}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-50 px-3 py-2 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/50 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#0a0a1a] px-3 py-2 text-sm font-medium text-blue-400 transition-colors hover:bg-[#0a0a1a] cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
|
|
@ -369,24 +369,24 @@ export default function JsonToCsvPage() {
|
|||
<button
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-rose-950/30 dark:hover:text-rose-400 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#111] px-3 py-2 text-sm font-medium text-gray-400 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-400">
|
||||
{/* Delimiter */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor={delimiterId} className="text-xs font-semibold text-gray-500 uppercase dark:text-gray-400">
|
||||
<label htmlFor={delimiterId} className="text-xs font-semibold text-gray-500 uppercase">
|
||||
Delimiter:
|
||||
</label>
|
||||
<select
|
||||
id={delimiterId}
|
||||
value={delimiter}
|
||||
onChange={(e) => setDelimiter(e.target.value)}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value=",">Comma (,)</option>
|
||||
<option value=";">Semicolon (;)</option>
|
||||
|
|
@ -397,14 +397,14 @@ export default function JsonToCsvPage() {
|
|||
|
||||
{/* Array Mode */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor={arrayModeId} className="text-xs font-semibold text-gray-500 uppercase dark:text-gray-400">
|
||||
<label htmlFor={arrayModeId} className="text-xs font-semibold text-gray-500 uppercase">
|
||||
Arrays:
|
||||
</label>
|
||||
<select
|
||||
id={arrayModeId}
|
||||
value={arrayMode}
|
||||
onChange={(e) => setArrayMode(e.target.value as "join" | "json")}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value="join">Join with ";"</option>
|
||||
<option value="json">JSON String</option>
|
||||
|
|
@ -414,33 +414,33 @@ export default function JsonToCsvPage() {
|
|||
</div>
|
||||
|
||||
{/* Toggles Strip */}
|
||||
<div className="flex flex-wrap items-center gap-5 text-xs text-gray-600 dark:text-gray-400">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<div className="flex flex-wrap items-center gap-5 text-xs text-gray-600">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={flatten}
|
||||
onChange={(e) => setFlatten(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Flatten nested objects (e.g. <code>address.city</code>)</span>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeHeaders}
|
||||
onChange={(e) => setIncludeHeaders(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Include header row</span>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-900 dark:hover:text-gray-100">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={quoteAll}
|
||||
onChange={(e) => setQuoteAll(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Quote all fields</span>
|
||||
</label>
|
||||
|
|
@ -448,9 +448,9 @@ export default function JsonToCsvPage() {
|
|||
|
||||
{/* Status Notification Banner */}
|
||||
{status.type === "success" && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600 dark:text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
|
|
@ -459,7 +459,7 @@ export default function JsonToCsvPage() {
|
|||
</svg>
|
||||
<span className="font-medium">{status.message}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800">
|
||||
{status.rowCount !== undefined && <span>Rows: <strong>{status.rowCount}</strong></span>}
|
||||
{status.colCount !== undefined && <span>Columns: <strong>{status.colCount}</strong></span>}
|
||||
{status.byteSize !== undefined && <span>Size: <strong>{formatBytes(status.byteSize)}</strong></span>}
|
||||
|
|
@ -468,9 +468,9 @@ export default function JsonToCsvPage() {
|
|||
)}
|
||||
|
||||
{status.type === "error" && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-900 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300">
|
||||
<div className=" border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
|
|
@ -478,10 +478,10 @@ export default function JsonToCsvPage() {
|
|||
/>
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-red-800 dark:text-red-200">Error Parsing JSON</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-700 dark:text-red-300">{status.message}</p>
|
||||
<p className="font-semibold text-red-400">Error Parsing JSON</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-400">{status.message}</p>
|
||||
{(status.line !== undefined || status.column !== undefined) && (
|
||||
<div className="mt-2 inline-flex items-center gap-2 rounded bg-red-100 px-2.5 py-1 text-xs font-medium text-red-800 dark:bg-red-900/60 dark:text-red-200">
|
||||
<div className="mt-2 inline-flex items-center gap-2 bg-[#1a0a0a] px-2.5 py-1 text-xs font-medium text-red-400">
|
||||
<span>📍 Location:</span>
|
||||
{status.line !== undefined && <span>Line <strong>{status.line}</strong></span>}
|
||||
{status.column !== undefined && <span>Column <strong>{status.column}</strong></span>}
|
||||
|
|
@ -498,8 +498,8 @@ export default function JsonToCsvPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Input JSON</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Input JSON</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({inputLineCount} {inputLineCount === 1 ? "line" : "lines"}, {input.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -513,14 +513,14 @@ export default function JsonToCsvPage() {
|
|||
// Clipboard permission denied
|
||||
}
|
||||
}}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-[#111] cursor-pointer"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setInput("")}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-red-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-red-400 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-[#111] hover:text-red-400 cursor-pointer"
|
||||
title="Clear input"
|
||||
>
|
||||
Clear
|
||||
|
|
@ -533,7 +533,7 @@ export default function JsonToCsvPage() {
|
|||
placeholder="Paste JSON array of objects here, e.g. [ { "id": 1, "name": "Alice" }, ... ]"
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-white p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -541,8 +541,8 @@ export default function JsonToCsvPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">CSV Output</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">CSV Output</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({outputLineCount} {outputLineCount === 1 ? "line" : "lines"}, {output.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -550,7 +550,7 @@ export default function JsonToCsvPage() {
|
|||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!output}
|
||||
className="inline-flex items-center gap-1 rounded px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:text-gray-300 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium text-gray-400 hover:bg-[#111] disabled:opacity-40 cursor-pointer"
|
||||
title="Download as CSV file"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -561,7 +561,7 @@ export default function JsonToCsvPage() {
|
|||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={!output}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
className="inline-flex items-center gap-1 bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
title="Copy CSV to clipboard"
|
||||
>
|
||||
{copied ? (
|
||||
|
|
@ -593,29 +593,29 @@ export default function JsonToCsvPage() {
|
|||
placeholder="Converted CSV tabular data will appear here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-gray-50 p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-900/80 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-black p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Highlights & Guide */}
|
||||
<div className="mt-8 rounded-xl border border-gray-200 bg-gray-50/70 p-5 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-800/30 dark:text-gray-400">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">How to Convert JSON to CSV</h3>
|
||||
<div className="mt-8 border border-[#1a1a1a] bg-black/70 p-5 text-sm text-gray-600">
|
||||
<h3 className="font-semibold text-gray-200">How to Convert JSON to CSV</h3>
|
||||
<div className="mt-3 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🌿 Nested Object Flattening</p>
|
||||
<p className="font-medium text-gray-300">🌿 Nested Object Flattening</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Automatically unpacks deep nested objects into flat dot-notation headers such as <code>user.address.geo.lat</code>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🛡️ RFC 4180 Escaping</p>
|
||||
<p className="font-medium text-gray-300">🛡️ RFC 4180 Escaping</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Safely handles quotes, multi-line values, commas, and special delimiters with compliant CSV double-quote escaping.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔒 100% Client-Side Privacy</p>
|
||||
<p className="font-medium text-gray-300">🔒 100% Client-Side Privacy</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Your data is processed directly inside your browser. No files or records are sent to any remote server.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -173,27 +173,27 @@ export default function JwtDecoderPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Samples & Actions Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-[#1a1a1a]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">Sample Tokens:</span>
|
||||
<span className="text-xs font-medium text-gray-500">Sample Tokens:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTokenInput(SAMPLE_VALID)}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 text-xs font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Active Token
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTokenInput(SAMPLE_EXPIRED)}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 text-xs font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Expired Token
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTokenInput(SAMPLE_FIREBASE)}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 text-xs font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
RS256 Token
|
||||
</button>
|
||||
|
|
@ -204,14 +204,14 @@ export default function JwtDecoderPage() {
|
|||
type="button"
|
||||
onClick={() => copyToClipboard(tokenInput, setCopiedToken)}
|
||||
disabled={!tokenInput}
|
||||
className="px-3 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-1.5 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copiedToken ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-green-600 dark:text-green-400 font-semibold">Copied!</span>
|
||||
<span className="text-green-400 font-semibold">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -226,7 +226,7 @@ export default function JwtDecoderPage() {
|
|||
type="button"
|
||||
onClick={() => setTokenInput("")}
|
||||
disabled={!tokenInput}
|
||||
className="px-3 py-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-950/30 text-gray-700 dark:text-gray-300 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed rounded text-xs font-medium transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 bg-[#111] hover:bg-[#1a0a0a] text-gray-400 hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed text-xs font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -236,23 +236,23 @@ export default function JwtDecoderPage() {
|
|||
{/* Input Area */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={inputId} className="font-semibold text-sm text-gray-800 dark:text-gray-200 flex items-center gap-2">
|
||||
<label htmlFor={inputId} className="font-semibold text-sm text-gray-300 flex items-center gap-2">
|
||||
<span>Encoded JWT Token</span>
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs font-normal text-gray-500">
|
||||
(Paste token below)
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-3 text-xs font-medium">
|
||||
<span className="inline-flex items-center gap-1 text-rose-600 dark:text-rose-400">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-rose-500 inline-block"></span>
|
||||
<span className="inline-flex items-center gap-1 text-rose-600">
|
||||
<span className="w-2.5 h-2.5 rounded-none bg-rose-500 inline-block"></span>
|
||||
Header
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 text-purple-600 dark:text-purple-400">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-purple-500 inline-block"></span>
|
||||
<span className="inline-flex items-center gap-1 text-purple-600">
|
||||
<span className="w-2.5 h-2.5 rounded-none bg-purple-500 inline-block"></span>
|
||||
Payload
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 text-sky-600 dark:text-sky-400">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-sky-500 inline-block"></span>
|
||||
<span className="inline-flex items-center gap-1 text-sky-600">
|
||||
<span className="w-2.5 h-2.5 rounded-none bg-sky-500 inline-block"></span>
|
||||
Signature
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -264,24 +264,24 @@ export default function JwtDecoderPage() {
|
|||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
placeholder="Paste a JSON Web Token here (e.g. eyJhbGciOi...)"
|
||||
rows={4}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
/>
|
||||
|
||||
{/* Color-Coded Token Preview Bar */}
|
||||
{tokenInput.trim() && parts.length === 3 && (
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-900/70 border border-gray-200 dark:border-gray-700/70 rounded-lg font-mono text-xs break-all leading-relaxed">
|
||||
<span className="text-rose-600 dark:text-rose-400 font-semibold">{parts[0]}</span>
|
||||
<span className="text-gray-400 dark:text-gray-600 font-bold">.</span>
|
||||
<span className="text-purple-600 dark:text-purple-400 font-semibold">{parts[1]}</span>
|
||||
<span className="text-gray-400 dark:text-gray-600 font-bold">.</span>
|
||||
<span className="text-sky-600 dark:text-sky-400 font-semibold">{parts[2]}</span>
|
||||
<div className="p-3 bg-black border border-[#1a1a1a] font-mono text-xs break-all leading-relaxed">
|
||||
<span className="text-rose-600 font-semibold">{parts[0]}</span>
|
||||
<span className="text-gray-400 font-bold">.</span>
|
||||
<span className="text-purple-600 font-semibold">{parts[1]}</span>
|
||||
<span className="text-gray-400 font-bold">.</span>
|
||||
<span className="text-sky-600 font-semibold">{parts[2]}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error Notification */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-800 rounded-lg text-red-800 dark:text-red-300 text-sm">
|
||||
<div className="flex items-start gap-3 p-4 bg-[#1a0a0a] border border-red-900 text-red-400 text-sm">
|
||||
<svg className="w-5 h-5 text-red-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
|
@ -297,18 +297,18 @@ export default function JwtDecoderPage() {
|
|||
<div>
|
||||
{expInfo ? (
|
||||
<div
|
||||
className={`p-4 rounded-xl border flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 ${
|
||||
className={`p-4 border flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 ${
|
||||
expInfo.isExpired
|
||||
? "bg-red-50 dark:bg-red-950/40 border-red-200 dark:border-red-800 text-red-900 dark:text-red-300"
|
||||
: "bg-emerald-50 dark:bg-emerald-950/40 border-emerald-200 dark:border-emerald-800 text-emerald-900 dark:text-emerald-300"
|
||||
? "bg-[#1a0a0a] border-red-900 text-red-900"
|
||||
: "bg-emerald-50 border-emerald-200 text-emerald-900"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`w-9 h-9 rounded-lg flex items-center justify-center shrink-0 ${
|
||||
className={`w-9 h-9 flex items-center justify-center shrink-0 ${
|
||||
expInfo.isExpired
|
||||
? "bg-red-100 dark:bg-red-900/60 text-red-600 dark:text-red-300"
|
||||
: "bg-emerald-100 dark:bg-emerald-900/60 text-emerald-600 dark:text-emerald-300"
|
||||
? "bg-[#1a0a0a] text-red-400"
|
||||
: "bg-emerald-100 text-emerald-600"
|
||||
}`}
|
||||
>
|
||||
{expInfo.isExpired ? (
|
||||
|
|
@ -324,7 +324,7 @@ export default function JwtDecoderPage() {
|
|||
<div>
|
||||
<h3 className="font-bold text-base flex items-center gap-2">
|
||||
<span>{expInfo.isExpired ? "Token is Expired" : "Token is Active"}</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded-full font-semibold uppercase tracking-wide bg-white/60 dark:bg-black/30">
|
||||
<span className="text-xs px-2 py-0.5 rounded-none font-semibold uppercase tracking-wide bg-[#0a0a0a]/60">
|
||||
{expInfo.relative}
|
||||
</span>
|
||||
</h3>
|
||||
|
|
@ -341,11 +341,11 @@ export default function JwtDecoderPage() {
|
|||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-3.5 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 dark:border-amber-800 rounded-xl text-amber-900 dark:text-amber-300 text-xs flex items-center gap-2">
|
||||
<svg className="w-4 h-4 shrink-0 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="p-3.5 bg-amber-50 border border-amber-200 text-amber-900 text-xs flex items-center gap-2">
|
||||
<svg className="w-4 h-4 shrink-0 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>No expiration (<code className="font-mono bg-amber-100 dark:bg-amber-900/60 px-1 py-0.5 rounded">exp</code>) claim present in this token payload.</span>
|
||||
<span>No expiration (<code className="font-mono bg-amber-100 px-1 py-0.5 rounded">exp</code>) claim present in this token payload.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -357,11 +357,11 @@ export default function JwtDecoderPage() {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full bg-rose-500"></span>
|
||||
<h3 className="font-bold text-sm text-gray-900 dark:text-gray-100">
|
||||
<span className="w-3 h-3 rounded-none bg-rose-500"></span>
|
||||
<h3 className="font-bold text-sm text-gray-200">
|
||||
Decoded Header
|
||||
</h3>
|
||||
<span className="text-xs text-rose-600 dark:text-rose-400 font-mono">
|
||||
<span className="text-xs text-rose-600 font-mono">
|
||||
(Algorithm & Token Type)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -369,14 +369,14 @@ export default function JwtDecoderPage() {
|
|||
type="button"
|
||||
onClick={() => copyToClipboard(headerString, setCopiedHeader)}
|
||||
disabled={!headerString}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copiedHeader ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-green-600 dark:text-green-400 font-semibold">Copied!</span>
|
||||
<span className="text-green-400 font-semibold">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -389,19 +389,19 @@ export default function JwtDecoderPage() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<pre className="p-4 rounded-lg border border-rose-200 dark:border-rose-900/50 bg-rose-50/50 dark:bg-rose-950/20 text-rose-950 dark:text-rose-200 font-mono text-xs leading-relaxed min-h-[140px] overflow-auto select-all shadow-inner">
|
||||
<pre className="p-4 border border-rose-200 bg-rose-50/50 text-rose-950 font-mono text-xs leading-relaxed min-h-[140px] overflow-auto select-all shadow-inner">
|
||||
{headerString || "// Decoded header JSON will appear here"}
|
||||
</pre>
|
||||
|
||||
{token?.header && (
|
||||
<div className="grid grid-cols-2 gap-2 text-xs font-mono">
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-gray-500 dark:text-gray-400">Algorithm (alg):</span>{" "}
|
||||
<span className="font-semibold text-rose-600 dark:text-rose-400">{String(token.header.alg || "none")}</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a]">
|
||||
<span className="text-gray-500">Algorithm (alg):</span>{" "}
|
||||
<span className="font-semibold text-rose-600">{String(token.header.alg || "none")}</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-gray-500 dark:text-gray-400">Type (typ):</span>{" "}
|
||||
<span className="font-semibold text-rose-600 dark:text-rose-400">{String(token.header.typ || "JWT")}</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a]">
|
||||
<span className="text-gray-500">Type (typ):</span>{" "}
|
||||
<span className="font-semibold text-rose-600">{String(token.header.typ || "JWT")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -411,11 +411,11 @@ export default function JwtDecoderPage() {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full bg-purple-500"></span>
|
||||
<h3 className="font-bold text-sm text-gray-900 dark:text-gray-100">
|
||||
<span className="w-3 h-3 rounded-none bg-purple-500"></span>
|
||||
<h3 className="font-bold text-sm text-gray-200">
|
||||
Decoded Payload
|
||||
</h3>
|
||||
<span className="text-xs text-purple-600 dark:text-purple-400 font-mono">
|
||||
<span className="text-xs text-purple-600 font-mono">
|
||||
(Data Claims)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -423,14 +423,14 @@ export default function JwtDecoderPage() {
|
|||
type="button"
|
||||
onClick={() => copyToClipboard(payloadString, setCopiedPayload)}
|
||||
disabled={!payloadString}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copiedPayload ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-green-600 dark:text-green-400 font-semibold">Copied!</span>
|
||||
<span className="text-green-400 font-semibold">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -443,34 +443,34 @@ export default function JwtDecoderPage() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<pre className="p-4 rounded-lg border border-purple-200 dark:border-purple-900/50 bg-purple-50/50 dark:bg-purple-950/20 text-purple-950 dark:text-purple-200 font-mono text-xs leading-relaxed min-h-[140px] overflow-auto select-all shadow-inner">
|
||||
<pre className="p-4 border border-purple-200 bg-purple-50/50 text-purple-950 font-mono text-xs leading-relaxed min-h-[140px] overflow-auto select-all shadow-inner">
|
||||
{payloadString || "// Decoded payload JSON will appear here"}
|
||||
</pre>
|
||||
|
||||
{token?.payload && (
|
||||
<div className="space-y-1.5 text-xs font-mono">
|
||||
{token.payload.sub !== undefined && (
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-500 dark:text-gray-400">Subject (sub):</span>
|
||||
<span className="font-semibold text-purple-600 dark:text-purple-400">{String(token.payload.sub)}</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-500">Subject (sub):</span>
|
||||
<span className="font-semibold text-purple-600">{String(token.payload.sub)}</span>
|
||||
</div>
|
||||
)}
|
||||
{token.payload.iss !== undefined && (
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-500 dark:text-gray-400">Issuer (iss):</span>
|
||||
<span className="font-semibold text-purple-600 dark:text-purple-400">{String(token.payload.iss)}</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-500">Issuer (iss):</span>
|
||||
<span className="font-semibold text-purple-600">{String(token.payload.iss)}</span>
|
||||
</div>
|
||||
)}
|
||||
{token.payload.aud !== undefined && (
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-500 dark:text-gray-400">Audience (aud):</span>
|
||||
<span className="font-semibold text-purple-600 dark:text-purple-400">{String(token.payload.aud)}</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-500">Audience (aud):</span>
|
||||
<span className="font-semibold text-purple-600">{String(token.payload.aud)}</span>
|
||||
</div>
|
||||
)}
|
||||
{nbfInfo && (
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-500 dark:text-gray-400">Not Before (nbf):</span>
|
||||
<span className="font-semibold text-purple-600 dark:text-purple-400">{nbfInfo.formatted}</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-500">Not Before (nbf):</span>
|
||||
<span className="font-semibold text-purple-600">{nbfInfo.formatted}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -480,34 +480,34 @@ export default function JwtDecoderPage() {
|
|||
|
||||
{/* Signature Inspection Card */}
|
||||
{token && (
|
||||
<div className="p-4 rounded-xl border border-sky-200 dark:border-sky-900/50 bg-sky-50/40 dark:bg-sky-950/20 space-y-2">
|
||||
<div className="p-4 border border-sky-200 bg-sky-50/40 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-3 h-3 rounded-full bg-sky-500"></span>
|
||||
<h4 className="font-bold text-sm text-gray-900 dark:text-gray-100">
|
||||
<span className="w-3 h-3 rounded-none bg-sky-500"></span>
|
||||
<h4 className="font-bold text-sm text-gray-200">
|
||||
Signature Component
|
||||
</h4>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<p className="text-xs text-gray-600">
|
||||
The signature is calculated by taking the encoded header, the encoded payload, a secret key, and signing them using the specified algorithm ({String(token.header?.alg || "HS256")}).
|
||||
</p>
|
||||
<div className="p-2.5 bg-white dark:bg-gray-900 rounded border border-sky-200 dark:border-sky-800 font-mono text-xs text-sky-700 dark:text-sky-300 break-all select-all">
|
||||
<div className="p-2.5 bg-[#0a0a0a] border border-sky-200 font-mono text-xs text-sky-700 break-all select-all">
|
||||
{token.signature || "// No signature"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informative Guide */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800 grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-800/40 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">🔴 Header (Red)</h4>
|
||||
<div className="mt-8 pt-6 border-t border-[#1a1a1a] grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-gray-600">
|
||||
<div className="p-3 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">🔴 Header (Red)</h4>
|
||||
<p>Contains metadata about the token, such as the signing algorithm (e.g. HS256, RS256) and token type (JWT).</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-800/40 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">🟣 Payload (Purple)</h4>
|
||||
<div className="p-3 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">🟣 Payload (Purple)</h4>
|
||||
<p>Contains the claims and entity statements (user ID, roles, permissions, expiration timestamps).</p>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 dark:bg-gray-800/40 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">🔵 Signature (Blue)</h4>
|
||||
<div className="p-3 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">🔵 Signature (Blue)</h4>
|
||||
<p>Cryptographic hash used by the server to verify that the message wasn't changed along the way.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -159,11 +159,11 @@ export default function LoremIpsumPage() {
|
|||
>
|
||||
<div className="space-y-6">
|
||||
{/* Controls Section */}
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/60 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<div className=" border border-[#1a1a1a] bg-black/60 p-4">
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-4">
|
||||
{/* Number of Paragraphs */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Paragraphs (1 - 20)
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -176,7 +176,7 @@ export default function LoremIpsumPage() {
|
|||
const val = Math.min(Math.max(1, parseInt(e.target.value) || 1), 20);
|
||||
setNumParagraphs(val);
|
||||
}}
|
||||
className="w-20 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-20 border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{[1, 3, 5, 10].map((count) => (
|
||||
|
|
@ -187,7 +187,7 @@ export default function LoremIpsumPage() {
|
|||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
numParagraphs === count
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
: "bg-[#1a1a1a] text-gray-400 hover:bg-[#222]"
|
||||
}`}
|
||||
>
|
||||
{count}
|
||||
|
|
@ -199,13 +199,13 @@ export default function LoremIpsumPage() {
|
|||
|
||||
{/* Paragraph Length */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Paragraph Length
|
||||
</label>
|
||||
<select
|
||||
value={lengthPreset}
|
||||
onChange={(e) => setLengthPreset(e.target.value as ParagraphLength)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
>
|
||||
<option value="short">Short (~35 words)</option>
|
||||
<option value="medium">Medium (~75 words)</option>
|
||||
|
|
@ -215,7 +215,7 @@ export default function LoremIpsumPage() {
|
|||
|
||||
{lengthPreset === "custom" && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Words:</span>
|
||||
<span className="text-xs text-gray-500">Words:</span>
|
||||
<input
|
||||
type="number"
|
||||
min="10"
|
||||
|
|
@ -224,7 +224,7 @@ export default function LoremIpsumPage() {
|
|||
onChange={(e) =>
|
||||
setCustomWords(Math.min(Math.max(10, parseInt(e.target.value) || 10), 300))
|
||||
}
|
||||
className="w-24 rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-900 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-24 border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs font-medium text-gray-200 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -232,17 +232,17 @@ export default function LoremIpsumPage() {
|
|||
|
||||
{/* Output Format */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Format
|
||||
</label>
|
||||
<div className="flex rounded-lg border border-gray-300 bg-gray-100 p-0.5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className="flex border border-[#1a1a1a] bg-[#111] p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFormat("plain")}
|
||||
className={`flex-1 rounded-md py-1.5 text-xs font-medium transition-colors ${
|
||||
className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
|
||||
format === "plain"
|
||||
? "bg-white text-gray-900 shadow-xs dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Plain Text
|
||||
|
|
@ -250,10 +250,10 @@ export default function LoremIpsumPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setFormat("html")}
|
||||
className={`flex-1 rounded-md py-1.5 text-xs font-medium transition-colors ${
|
||||
className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
|
||||
format === "html"
|
||||
? "bg-white text-gray-900 shadow-xs dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-gray-200 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
HTML <p>
|
||||
|
|
@ -263,15 +263,15 @@ export default function LoremIpsumPage() {
|
|||
|
||||
{/* Action Options */}
|
||||
<div className="flex flex-col justify-between">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Options
|
||||
</label>
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={startWithLorem}
|
||||
onChange={(e) => setStartWithLorem(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Start with “Lorem ipsum...”</span>
|
||||
</label>
|
||||
|
|
@ -279,7 +279,7 @@ export default function LoremIpsumPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={generate}
|
||||
className="mt-3 flex w-full items-center justify-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
className="mt-3 flex w-full items-center justify-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-1 focus:ring-blue-900 focus:ring-offset-2"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -296,18 +296,18 @@ export default function LoremIpsumPage() {
|
|||
</div>
|
||||
|
||||
{/* Output Section Header with Stats & Actions */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-3 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="inline-flex items-center gap-1 font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">{paragraphCount}</span> paragraphs
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-3">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-gray-500">
|
||||
<span className="inline-flex items-center gap-1 font-medium text-gray-400">
|
||||
<span className="font-semibold text-blue-600">{paragraphCount}</span> paragraphs
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span className="inline-flex items-center gap-1 font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">{wordCount}</span> words
|
||||
<span className="inline-flex items-center gap-1 font-medium text-gray-400">
|
||||
<span className="font-semibold text-blue-600">{wordCount}</span> words
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span className="inline-flex items-center gap-1 font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">{charCount}</span> characters
|
||||
<span className="inline-flex items-center gap-1 font-medium text-gray-400">
|
||||
<span className="font-semibold text-blue-600">{charCount}</span> characters
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -315,7 +315,7 @@ export default function LoremIpsumPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-medium transition-all ${
|
||||
className={`inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium transition-all ${
|
||||
copied
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-blue-600 text-white hover:bg-blue-700 active:scale-95"
|
||||
|
|
@ -347,7 +347,7 @@ export default function LoremIpsumPage() {
|
|||
type="button"
|
||||
onClick={handleDownload}
|
||||
title="Download text file"
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
className="inline-flex items-center gap-1.5 bg-[#111] px-3 py-2 text-sm font-medium text-gray-400 transition-colors hover:bg-[#1a1a1a]"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -368,21 +368,21 @@ export default function LoremIpsumPage() {
|
|||
readOnly
|
||||
value={generatedText}
|
||||
rows={12}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white p-4 font-sans text-sm leading-relaxed text-gray-900 shadow-inner focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800/80 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] p-4 font-sans text-sm leading-relaxed text-gray-200 shadow-inner focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
aria-label="Generated Lorem Ipsum Text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Informational Details */}
|
||||
<div className="grid grid-cols-1 gap-4 pt-2 text-xs text-gray-500 dark:text-gray-400 sm:grid-cols-2">
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3.5 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">Origin of Lorem Ipsum</h4>
|
||||
<div className="grid grid-cols-1 gap-4 pt-2 text-xs text-gray-500 sm:grid-cols-2">
|
||||
<div className=" border border-gray-100 bg-black p-3.5">
|
||||
<h4 className="font-semibold text-gray-300">Origin of Lorem Ipsum</h4>
|
||||
<p className="mt-1 leading-normal">
|
||||
Lorem Ipsum has been standard placeholder text since the 1500s, adapted from Cicero's philosophical work <em>“de Finibus Bonorum et Malorum”</em> written in 45 BC.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3.5 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">Design & Layout Utility</h4>
|
||||
<div className=" border border-gray-100 bg-black p-3.5">
|
||||
<h4 className="font-semibold text-gray-300">Design & Layout Utility</h4>
|
||||
<p className="mt-1 leading-normal">
|
||||
Using realistic filler text with natural word and sentence distribution avoids distracting reviewers with readable content while focusing purely on layout and typography.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -382,45 +382,45 @@ ${renderedHtml}
|
|||
` }} />
|
||||
|
||||
{/* Top Control Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-3 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-3">
|
||||
{/* View Mode Switcher */}
|
||||
<div className="flex items-center rounded-lg bg-gray-100 p-1 dark:bg-gray-800">
|
||||
<div className="flex items-center bg-[#111] p-1">
|
||||
<button
|
||||
onClick={() => setViewMode("split")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium transition-all ${
|
||||
className={` px-3 py-1 text-xs font-medium transition-all ${
|
||||
viewMode === "split"
|
||||
? "bg-white text-blue-600 shadow-xs dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Split View
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("preview")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium transition-all ${
|
||||
className={` px-3 py-1 text-xs font-medium transition-all ${
|
||||
viewMode === "preview"
|
||||
? "bg-white text-blue-600 shadow-xs dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("rawHtml")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium transition-all ${
|
||||
className={` px-3 py-1 text-xs font-medium transition-all ${
|
||||
viewMode === "rawHtml"
|
||||
? "bg-white text-blue-600 shadow-xs dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
HTML Code
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode("edit")}
|
||||
className={`rounded-md px-3 py-1 text-xs font-medium transition-all ${
|
||||
className={` px-3 py-1 text-xs font-medium transition-all ${
|
||||
viewMode === "edit"
|
||||
? "bg-white text-blue-600 shadow-xs dark:bg-gray-700 dark:text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Editor Only
|
||||
|
|
@ -431,7 +431,7 @@ ${renderedHtml}
|
|||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopyHtml}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3.5 py-1.5 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95"
|
||||
className="inline-flex items-center gap-1.5 bg-blue-600 px-3.5 py-1.5 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95"
|
||||
>
|
||||
{copiedHtml ? (
|
||||
<>
|
||||
|
|
@ -457,14 +457,14 @@ ${renderedHtml}
|
|||
|
||||
<button
|
||||
onClick={handleCopyMarkdown}
|
||||
className="inline-flex items-center gap-1 rounded-lg bg-gray-200 px-3 py-1.5 text-xs font-medium text-gray-800 transition-colors hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
className="inline-flex items-center gap-1 bg-[#1a1a1a] px-3 py-1.5 text-xs font-medium text-gray-300 transition-colors hover:bg-[#222]"
|
||||
>
|
||||
{copiedMd ? "Markdown Copied!" : "Copy Markdown"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDownloadHtml}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="inline-flex items-center gap-1 border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 text-xs font-medium text-gray-400 hover:bg-black"
|
||||
title="Download standalone HTML document"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -475,7 +475,7 @@ ${renderedHtml}
|
|||
|
||||
<button
|
||||
onClick={handleDownloadMd}
|
||||
className="inline-flex items-center gap-1 rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="inline-flex items-center gap-1 border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 text-xs font-medium text-gray-400 hover:bg-black"
|
||||
title="Download markdown file"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -486,14 +486,14 @@ ${renderedHtml}
|
|||
|
||||
<button
|
||||
onClick={() => setMarkdown(SAMPLE_MARKDOWN)}
|
||||
className="rounded-md px-2.5 py-1 text-xs font-medium text-gray-600 underline-offset-2 hover:text-blue-600 hover:underline dark:text-gray-400 dark:hover:text-blue-400"
|
||||
className=" px-2.5 py-1 text-xs font-medium text-gray-600 underline-offset-2 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
Reset Sample
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setMarkdown("")}
|
||||
className="rounded-md px-2.5 py-1 text-xs font-medium text-red-600 underline-offset-2 hover:text-red-700 hover:underline dark:text-red-400 dark:hover:text-red-300"
|
||||
className=" px-2.5 py-1 text-xs font-medium text-red-400 underline-offset-2 hover:text-red-400 hover:underline"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -501,12 +501,12 @@ ${renderedHtml}
|
|||
</div>
|
||||
|
||||
{/* Quick Markdown Insertion Toolbar */}
|
||||
<div className="flex flex-wrap items-center gap-1 rounded-lg border border-gray-200 bg-gray-50/70 p-1.5 text-xs dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<span className="px-2 font-medium text-gray-500 dark:text-gray-400">Insert:</span>
|
||||
<div className="flex flex-wrap items-center gap-1 border border-[#1a1a1a] bg-black/70 p-1.5 text-xs">
|
||||
<span className="px-2 font-medium text-gray-500">Insert:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("**", "**", "bold text")}
|
||||
className="rounded px-2 py-1 font-bold text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 font-bold text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Bold"
|
||||
>
|
||||
B
|
||||
|
|
@ -514,7 +514,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("*", "*", "italic text")}
|
||||
className="rounded px-2 py-1 italic text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 italic text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Italic"
|
||||
>
|
||||
I
|
||||
|
|
@ -522,7 +522,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("## ", "\n", "Heading 2")}
|
||||
className="rounded px-2 py-1 font-semibold text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 font-semibold text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Heading 2"
|
||||
>
|
||||
H2
|
||||
|
|
@ -530,7 +530,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("### ", "\n", "Heading 3")}
|
||||
className="rounded px-2 py-1 font-semibold text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 font-semibold text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Heading 3"
|
||||
>
|
||||
H3
|
||||
|
|
@ -538,7 +538,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("[", "](https://example.com)", "link title")}
|
||||
className="rounded px-2 py-1 text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Link"
|
||||
>
|
||||
🔗 Link
|
||||
|
|
@ -546,7 +546,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("\n```javascript\n", "\n```\n", "// code here")}
|
||||
className="rounded px-2 py-1 font-mono text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 font-mono text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Code Block"
|
||||
>
|
||||
{"</>"} Code
|
||||
|
|
@ -554,7 +554,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("\n> ", "\n", "Quote text")}
|
||||
className="rounded px-2 py-1 text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Blockquote"
|
||||
>
|
||||
❝ Quote
|
||||
|
|
@ -562,7 +562,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("- ", "\n", "List item")}
|
||||
className="rounded px-2 py-1 text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Bullet list"
|
||||
>
|
||||
• List
|
||||
|
|
@ -570,7 +570,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("- [ ] ", "\n", "Todo task")}
|
||||
className="rounded px-2 py-1 text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Checklist task"
|
||||
>
|
||||
☑ Task
|
||||
|
|
@ -578,7 +578,7 @@ ${renderedHtml}
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => insertSnippet("\n| Column 1 | Column 2 |\n| :--- | :--- |\n| Value 1 | Value 2 |\n", "")}
|
||||
className="rounded px-2 py-1 text-gray-700 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded px-2 py-1 text-gray-400 hover:bg-[#1a1a1a]"
|
||||
title="Table"
|
||||
>
|
||||
▦ Table
|
||||
|
|
@ -597,10 +597,10 @@ ${renderedHtml}
|
|||
{(viewMode === "split" || viewMode === "edit") && (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<span className="text-sm font-semibold text-gray-300">
|
||||
Markdown Source
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500">
|
||||
{stats.lines} lines · {stats.words} words · {stats.chars} chars
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -611,7 +611,7 @@ ${renderedHtml}
|
|||
placeholder="Type your markdown here..."
|
||||
rows={22}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-white p-4 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-[#0a0a0a] p-4 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -620,15 +620,15 @@ ${renderedHtml}
|
|||
{(viewMode === "split" || viewMode === "preview") && (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<span className="text-sm font-semibold text-gray-300">
|
||||
Rendered Preview
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500">
|
||||
Estimated read time: ~{stats.readingTimeMinutes} min
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="markdown-preview-output min-h-[500px] flex-1 overflow-y-auto rounded-lg border border-gray-300 bg-white p-5 text-gray-900 shadow-xs dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
|
||||
className="markdown-preview-output min-h-[500px] flex-1 overflow-y-auto border border-[#1a1a1a] bg-[#0a0a0a] p-5 text-gray-200 shadow-xs"
|
||||
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -637,10 +637,10 @@ ${renderedHtml}
|
|||
{viewMode === "rawHtml" && (
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">
|
||||
<span className="text-sm font-semibold text-gray-300">
|
||||
Generated HTML Code
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500">
|
||||
{renderedHtml.length} characters
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -648,30 +648,30 @@ ${renderedHtml}
|
|||
value={renderedHtml}
|
||||
readOnly
|
||||
rows={22}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-gray-50 p-4 font-mono text-xs leading-relaxed text-gray-900 shadow-xs focus:outline-none dark:border-gray-700 dark:bg-gray-900/90 dark:text-gray-200"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-black p-4 font-mono text-xs leading-relaxed text-gray-200 shadow-xs focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Informational Footer */}
|
||||
<div className="mt-8 rounded-xl border border-gray-200 bg-gray-50/70 p-5 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-800/30 dark:text-gray-400">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">About Live Markdown Preview</h3>
|
||||
<div className="mt-8 border border-[#1a1a1a] bg-black/70 p-5 text-sm text-gray-600">
|
||||
<h3 className="font-semibold text-gray-200">About Live Markdown Preview</h3>
|
||||
<div className="mt-3 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">⚡ Live Rendering</p>
|
||||
<p className="font-medium text-gray-300">⚡ Live Rendering</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Real-time parsing as you type using the fast and robust marked compiler with Github Flavored Markdown (GFM) support.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">📋 Export & Share</p>
|
||||
<p className="font-medium text-gray-300">📋 Export & Share</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Instantly copy the generated HTML code or download standalone .html and .md files ready for publishing.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔒 100% Private</p>
|
||||
<p className="font-medium text-gray-300">🔒 100% Private</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Zero server requests. All markdown processing and parsing occurs strictly within your browser.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -162,17 +162,17 @@ export default function MetaTagGeneratorPage() {
|
|||
const descLen = state.description.length;
|
||||
|
||||
const getTitleStatus = (len: number) => {
|
||||
if (len === 0) return { label: "Empty", color: "text-gray-400", bg: "bg-gray-200 dark:bg-gray-700", border: "border-gray-300" };
|
||||
if (len <= 60) return { label: "Optimal (<60)", color: "text-emerald-600 dark:text-emerald-400", bg: "bg-emerald-500", border: "border-emerald-500" };
|
||||
if (len <= 70) return { label: "Slightly Long (61-70)", color: "text-amber-600 dark:text-amber-400", bg: "bg-amber-500", border: "border-amber-500" };
|
||||
return { label: "Too Long (>70)", color: "text-rose-600 dark:text-rose-400", bg: "bg-rose-500", border: "border-rose-500" };
|
||||
if (len === 0) return { label: "Empty", color: "text-gray-400", bg: "bg-[#1a1a1a]", border: "border-[#1a1a1a]" };
|
||||
if (len <= 60) return { label: "Optimal (<60)", color: "text-emerald-600", bg: "bg-emerald-500", border: "border-emerald-500" };
|
||||
if (len <= 70) return { label: "Slightly Long (61-70)", color: "text-amber-600", bg: "bg-amber-500", border: "border-amber-500" };
|
||||
return { label: "Too Long (>70)", color: "text-rose-600", bg: "bg-rose-500", border: "border-rose-500" };
|
||||
};
|
||||
|
||||
const getDescStatus = (len: number) => {
|
||||
if (len === 0) return { label: "Empty", color: "text-gray-400", bg: "bg-gray-200 dark:bg-gray-700", border: "border-gray-300" };
|
||||
if (len <= 160) return { label: "Optimal (<160)", color: "text-emerald-600 dark:text-emerald-400", bg: "bg-emerald-500", border: "border-emerald-500" };
|
||||
if (len <= 180) return { label: "Slightly Long (161-180)", color: "text-amber-600 dark:text-amber-400", bg: "bg-amber-500", border: "border-amber-500" };
|
||||
return { label: "Too Long (>180)", color: "text-rose-600 dark:text-rose-400", bg: "bg-rose-500", border: "border-rose-500" };
|
||||
if (len === 0) return { label: "Empty", color: "text-gray-400", bg: "bg-[#1a1a1a]", border: "border-[#1a1a1a]" };
|
||||
if (len <= 160) return { label: "Optimal (<160)", color: "text-emerald-600", bg: "bg-emerald-500", border: "border-emerald-500" };
|
||||
if (len <= 180) return { label: "Slightly Long (161-180)", color: "text-amber-600", bg: "bg-amber-500", border: "border-amber-500" };
|
||||
return { label: "Too Long (>180)", color: "text-rose-600", bg: "bg-rose-500", border: "border-rose-500" };
|
||||
};
|
||||
|
||||
const titleStatus = getTitleStatus(titleLen);
|
||||
|
|
@ -309,15 +309,15 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
<div className="space-y-8">
|
||||
{/* Top Controls and Presets */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-[#1a1a1a]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Presets:</span>
|
||||
<span className="text-xs font-semibold text-gray-500">Presets:</span>
|
||||
{SAMPLE_PRESETS.map((p) => (
|
||||
<button
|
||||
key={p.name}
|
||||
type="button"
|
||||
onClick={() => handleApplyPreset(p.state)}
|
||||
className="px-2.5 py-1 text-xs font-medium bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 rounded-md transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 text-xs font-medium bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
|
|
@ -328,7 +328,7 @@ export default function MetaTagGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleSyncToSocial}
|
||||
className="px-3 py-1 text-xs font-medium bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/50 dark:hover:bg-blue-900/60 text-blue-700 dark:text-blue-300 rounded-md transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-1 text-xs font-medium bg-[#0a0a1a] hover:bg-[#0a0a1a] text-blue-400 transition-colors cursor-pointer flex items-center gap-1"
|
||||
title="Copy Title & Description into Open Graph and Twitter Card fields"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -339,7 +339,7 @@ export default function MetaTagGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="px-3 py-1 text-xs font-medium bg-gray-100 hover:bg-rose-50 dark:bg-gray-800 dark:hover:bg-rose-950/30 text-gray-700 dark:text-gray-300 hover:text-rose-600 dark:hover:text-rose-400 rounded-md transition-colors cursor-pointer"
|
||||
className="px-3 py-1 text-xs font-medium bg-[#111] hover:bg-rose-50 text-gray-400 hover:text-rose-600 transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -351,12 +351,12 @@ export default function MetaTagGeneratorPage() {
|
|||
{/* Left Column: Form Fields (7 cols on large screens) */}
|
||||
<div className="lg:col-span-7 space-y-6">
|
||||
{/* Section 1: Basic SEO Meta Tags */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900/60 p-5 shadow-xs space-y-4">
|
||||
<div className="flex items-center gap-2 border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300 text-xs font-bold">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-4">
|
||||
<div className="flex items-center gap-2 border-b border-gray-100 pb-2">
|
||||
<span className="flex h-6 w-6 items-center justify-center bg-[#0a0a1a] text-blue-400 text-xs font-bold">
|
||||
1
|
||||
</span>
|
||||
<h2 className="text-sm font-bold text-gray-900 dark:text-white">
|
||||
<h2 className="text-sm font-bold text-gray-200">
|
||||
Basic SEO & Search Engine Tags
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -364,7 +364,7 @@ export default function MetaTagGeneratorPage() {
|
|||
{/* Title Field with Character Warning */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={titleId} className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={titleId} className="text-xs font-semibold text-gray-400">
|
||||
Page Title <span className="text-rose-500">*</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -379,10 +379,10 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.title}
|
||||
onChange={(e) => setState({ ...state, title: e.target.value })}
|
||||
placeholder="e.g. My Website — High Performance Developer Utilities"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none font-sans"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none font-sans"
|
||||
/>
|
||||
{/* Visual Progress Bar */}
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-none bg-[#111]">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${titleStatus.bg}`}
|
||||
style={{ width: `${Math.min((titleLen / 60) * 100, 100)}%` }}
|
||||
|
|
@ -393,7 +393,7 @@ export default function MetaTagGeneratorPage() {
|
|||
{/* Description Field with Character Warning */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={descId} className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={descId} className="text-xs font-semibold text-gray-400">
|
||||
Meta Description <span className="text-rose-500">*</span>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -408,10 +408,10 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.description}
|
||||
onChange={(e) => setState({ ...state, description: e.target.value })}
|
||||
placeholder="A concise summary of your webpage for search engine results..."
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y font-sans leading-relaxed"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y font-sans leading-relaxed"
|
||||
/>
|
||||
{/* Visual Progress Bar */}
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-none bg-[#111]">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${descStatus.bg}`}
|
||||
style={{ width: `${Math.min((descLen / 160) * 100, 100)}%` }}
|
||||
|
|
@ -422,7 +422,7 @@ export default function MetaTagGeneratorPage() {
|
|||
{/* Keywords and Author Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor={keywordsId} className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={keywordsId} className="text-xs font-semibold text-gray-400">
|
||||
Keywords (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -431,12 +431,12 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.keywords}
|
||||
onChange={(e) => setState({ ...state, keywords: e.target.value })}
|
||||
placeholder="tools, developer, seo, open graph"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label htmlFor={authorId} className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={authorId} className="text-xs font-semibold text-gray-400">
|
||||
Author
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -445,7 +445,7 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.author}
|
||||
onChange={(e) => setState({ ...state, author: e.target.value })}
|
||||
placeholder="Jane Doe or Organization"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -453,7 +453,7 @@ export default function MetaTagGeneratorPage() {
|
|||
{/* Canonical URL & Theme Color */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="sm:col-span-2 space-y-1">
|
||||
<label htmlFor={canonicalId} className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={canonicalId} className="text-xs font-semibold text-gray-400">
|
||||
Canonical URL
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -462,12 +462,12 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.canonicalUrl}
|
||||
onChange={(e) => setState({ ...state, canonicalUrl: e.target.value })}
|
||||
placeholder="https://example.com/page-url"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm font-mono focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Theme Color
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -475,80 +475,80 @@ export default function MetaTagGeneratorPage() {
|
|||
type="color"
|
||||
value={state.themeColor || "#2563eb"}
|
||||
onChange={(e) => setState({ ...state, themeColor: e.target.value })}
|
||||
className="h-9 w-9 p-0.5 rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 cursor-pointer"
|
||||
className="h-9 w-9 p-0.5 border border-[#1a1a1a] bg-[#0a0a0a] cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={state.themeColor}
|
||||
onChange={(e) => setState({ ...state, themeColor: e.target.value })}
|
||||
placeholder="#2563eb"
|
||||
className="flex-1 p-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="flex-1 p-2 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Robots Directives Checkboxes */}
|
||||
<div className="pt-2 border-t border-gray-100 dark:border-gray-800">
|
||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300 block mb-2">
|
||||
<div className="pt-2 border-t border-gray-100">
|
||||
<span className="text-xs font-semibold text-gray-400 block mb-2">
|
||||
Robots & Crawler Indexing Directives
|
||||
</span>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 text-xs">
|
||||
<label className="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700/60 bg-gray-50/50 dark:bg-gray-800/40 cursor-pointer select-none">
|
||||
<label className="flex items-center gap-2 p-2 border border-[#1a1a1a] bg-black/50 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.robotsIndex}
|
||||
onChange={(e) => setState({ ...state, robotsIndex: e.target.checked })}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||
<span className="font-medium text-gray-300">
|
||||
{state.robotsIndex ? "Index (allow)" : "NoIndex (block)"}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700/60 bg-gray-50/50 dark:bg-gray-800/40 cursor-pointer select-none">
|
||||
<label className="flex items-center gap-2 p-2 border border-[#1a1a1a] bg-black/50 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.robotsFollow}
|
||||
onChange={(e) => setState({ ...state, robotsFollow: e.target.checked })}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">
|
||||
<span className="font-medium text-gray-300">
|
||||
{state.robotsFollow ? "Follow links" : "NoFollow links"}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700/60 bg-gray-50/50 dark:bg-gray-800/40 cursor-pointer select-none">
|
||||
<label className="flex items-center gap-2 p-2 border border-[#1a1a1a] bg-black/50 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.robotsNoArchive}
|
||||
onChange={(e) => setState({ ...state, robotsNoArchive: e.target.checked })}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span className="text-gray-700 dark:text-gray-300">NoArchive</span>
|
||||
<span className="text-gray-400">NoArchive</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 p-2 rounded-lg border border-gray-200 dark:border-gray-700/60 bg-gray-50/50 dark:bg-gray-800/40 cursor-pointer select-none">
|
||||
<label className="flex items-center gap-2 p-2 border border-[#1a1a1a] bg-black/50 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.robotsNoSnippet}
|
||||
onChange={(e) => setState({ ...state, robotsNoSnippet: e.target.checked })}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span className="text-gray-700 dark:text-gray-300">NoSnippet</span>
|
||||
<span className="text-gray-400">NoSnippet</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Open Graph (Facebook / LinkedIn / Discord) */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900/60 p-5 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300 text-xs font-bold">
|
||||
<span className="flex h-6 w-6 items-center justify-center bg-indigo-100 text-indigo-700 text-xs font-bold">
|
||||
2
|
||||
</span>
|
||||
<h2 className="text-sm font-bold text-gray-900 dark:text-white">
|
||||
<h2 className="text-sm font-bold text-gray-200">
|
||||
Open Graph Tags (Facebook, LinkedIn, Discord)
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -557,13 +557,13 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
OG Type (og:type)
|
||||
</label>
|
||||
<select
|
||||
value={state.ogType}
|
||||
onChange={(e) => setState({ ...state, ogType: e.target.value })}
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
>
|
||||
<option value="website">website</option>
|
||||
<option value="article">article</option>
|
||||
|
|
@ -575,7 +575,7 @@ export default function MetaTagGeneratorPage() {
|
|||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Site Name (og:site_name)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -583,13 +583,13 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.ogSiteName}
|
||||
onChange={(e) => setState({ ...state, ogSiteName: e.target.value })}
|
||||
placeholder="e.g. My Awesome Site"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
OG Title (og:title)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -597,12 +597,12 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.ogTitle}
|
||||
onChange={(e) => setState({ ...state, ogTitle: e.target.value })}
|
||||
placeholder="Custom title for social shares..."
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
OG Description (og:description)
|
||||
</label>
|
||||
<textarea
|
||||
|
|
@ -610,13 +610,13 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.ogDescription}
|
||||
onChange={(e) => setState({ ...state, ogDescription: e.target.value })}
|
||||
placeholder="Custom description for social card shares..."
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y leading-relaxed"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
OG Image URL (og:image)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -624,12 +624,12 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.ogImage}
|
||||
onChange={(e) => setState({ ...state, ogImage: e.target.value })}
|
||||
placeholder="https://example.com/og-image.jpg"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
OG URL (og:url)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -637,20 +637,20 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.ogUrl}
|
||||
onChange={(e) => setState({ ...state, ogUrl: e.target.value })}
|
||||
placeholder="https://example.com/page"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Twitter / X Cards */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900/60 p-5 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md bg-sky-100 text-sky-700 dark:bg-sky-950 dark:text-sky-300 text-xs font-bold">
|
||||
<span className="flex h-6 w-6 items-center justify-center bg-sky-100 text-sky-700 text-xs font-bold">
|
||||
3
|
||||
</span>
|
||||
<h2 className="text-sm font-bold text-gray-900 dark:text-white">
|
||||
<h2 className="text-sm font-bold text-gray-200">
|
||||
Twitter / X Card Tags
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -659,7 +659,7 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Card Type
|
||||
</label>
|
||||
<select
|
||||
|
|
@ -670,7 +670,7 @@ export default function MetaTagGeneratorPage() {
|
|||
twitterCard: e.target.value as MetaTagState["twitterCard"],
|
||||
})
|
||||
}
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
>
|
||||
<option value="summary_large_image">summary_large_image (Large Hero)</option>
|
||||
<option value="summary">summary (Small Thumbnail)</option>
|
||||
|
|
@ -680,7 +680,7 @@ export default function MetaTagGeneratorPage() {
|
|||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Site Handle (@username)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -688,12 +688,12 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.twitterSite}
|
||||
onChange={(e) => setState({ ...state, twitterSite: e.target.value })}
|
||||
placeholder="@company"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm font-mono focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Creator Handle (@author)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -701,13 +701,13 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.twitterCreator}
|
||||
onChange={(e) => setState({ ...state, twitterCreator: e.target.value })}
|
||||
placeholder="@creator"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm font-mono focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm font-mono focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Twitter Card Title (optional override)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -715,12 +715,12 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.twitterTitle}
|
||||
onChange={(e) => setState({ ...state, twitterTitle: e.target.value })}
|
||||
placeholder="Leave empty or fill to override standard title..."
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Twitter Image URL
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -728,7 +728,7 @@ export default function MetaTagGeneratorPage() {
|
|||
value={state.twitterImage}
|
||||
onChange={(e) => setState({ ...state, twitterImage: e.target.value })}
|
||||
placeholder="https://example.com/twitter-image.jpg"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -738,14 +738,14 @@ export default function MetaTagGeneratorPage() {
|
|||
<div className="lg:col-span-5 space-y-4">
|
||||
<div className="sticky top-6 space-y-4">
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex rounded-xl border border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-900 p-1">
|
||||
<div className="flex border border-[#1a1a1a] bg-[#111] p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewTab("html")}
|
||||
className={`flex-1 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`flex-1 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
previewTab === "html"
|
||||
? "bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
HTML Tags
|
||||
|
|
@ -753,10 +753,10 @@ export default function MetaTagGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewTab("google")}
|
||||
className={`flex-1 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`flex-1 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
previewTab === "google"
|
||||
? "bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Google
|
||||
|
|
@ -764,10 +764,10 @@ export default function MetaTagGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewTab("facebook")}
|
||||
className={`flex-1 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`flex-1 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
previewTab === "facebook"
|
||||
? "bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Facebook / OG
|
||||
|
|
@ -775,10 +775,10 @@ export default function MetaTagGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setPreviewTab("twitter")}
|
||||
className={`flex-1 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`flex-1 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
previewTab === "twitter"
|
||||
? "bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-xs"
|
||||
: "text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"
|
||||
? "bg-[#0a0a0a] text-blue-600 shadow-xs"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Twitter / X
|
||||
|
|
@ -787,19 +787,19 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
{/* Tab 1: HTML Output */}
|
||||
{previewTab === "html" && (
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-gray-900 text-gray-100 overflow-hidden shadow-lg">
|
||||
<div className=" border border-[#1a1a1a] bg-gray-900 text-gray-100 overflow-hidden ">
|
||||
<div className="flex items-center justify-between px-4 py-2.5 bg-gray-950/80 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-3 w-3 rounded-full bg-red-500/80 inline-block" />
|
||||
<span className="h-3 w-3 rounded-full bg-amber-500/80 inline-block" />
|
||||
<span className="h-3 w-3 rounded-full bg-emerald-500/80 inline-block" />
|
||||
<span className="h-3 w-3 rounded-none bg-[#1a0a0a]0/80 inline-block" />
|
||||
<span className="h-3 w-3 rounded-none bg-amber-500/80 inline-block" />
|
||||
<span className="h-3 w-3 rounded-none bg-emerald-500/80 inline-block" />
|
||||
<span className="ml-2 text-xs font-mono text-gray-400"><head> tags</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
|
|
@ -829,21 +829,21 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
{/* Tab 2: Google Search Snippet Preview */}
|
||||
{previewTab === "google" && (
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||
Google Search Result Snippet
|
||||
</span>
|
||||
<span className="text-[11px] text-gray-400 font-mono">Desktop & Mobile</span>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-xl border border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-800/30 space-y-1.5 font-sans">
|
||||
<div className="p-4 border border-gray-100 bg-black/50 space-y-1.5 font-sans">
|
||||
{/* Breadcrumbs & URL */}
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400 truncate">
|
||||
<div className="w-4 h-4 rounded-full bg-blue-600 text-white text-[10px] flex items-center justify-center font-bold">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 truncate">
|
||||
<div className="w-4 h-4 rounded-none bg-blue-600 text-white text-[10px] flex items-center justify-center font-bold">
|
||||
G
|
||||
</div>
|
||||
<span className="text-gray-900 dark:text-gray-200 font-medium">
|
||||
<span className="text-gray-200 font-medium">
|
||||
{state.ogSiteName || "Website"}
|
||||
</span>
|
||||
<span className="text-gray-400">•</span>
|
||||
|
|
@ -853,17 +853,17 @@ export default function MetaTagGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Google Blue Link Title */}
|
||||
<h3 className="text-base sm:text-lg font-medium text-blue-700 hover:underline dark:text-blue-400 cursor-pointer line-clamp-1 leading-snug">
|
||||
<h3 className="text-base sm:text-lg font-medium text-blue-400 hover:underline cursor-pointer line-clamp-1 leading-snug">
|
||||
{state.title || "Untitled Page — Please enter a title"}
|
||||
</h3>
|
||||
|
||||
{/* Meta Description snippet */}
|
||||
<p className="text-xs sm:text-sm text-gray-600 dark:text-gray-300 line-clamp-2 leading-relaxed">
|
||||
<p className="text-xs sm:text-sm text-gray-600 line-clamp-2 leading-relaxed">
|
||||
{state.description || "No description provided yet. Enter a meta description to see how your site will appear in Google search results."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] text-gray-500 dark:text-gray-400 space-y-1 pt-2">
|
||||
<div className="text-[11px] text-gray-500 space-y-1 pt-2">
|
||||
<div>
|
||||
<strong>Google Title:</strong> {titleLen}/60 characters ({titleStatus.label})
|
||||
</div>
|
||||
|
|
@ -876,17 +876,17 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
{/* Tab 3: Facebook / Open Graph Card Preview */}
|
||||
{previewTab === "facebook" && (
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||
Facebook / LinkedIn / Discord Card
|
||||
</span>
|
||||
<span className="text-[11px] text-gray-400 font-mono">1200 × 630 px</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-850 overflow-hidden shadow-sm">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] overflow-hidden ">
|
||||
{/* Card Image Area */}
|
||||
<div className="aspect-[1.91/1] w-full bg-gray-200 dark:bg-gray-800 relative flex items-center justify-center overflow-hidden border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="aspect-[1.91/1] w-full bg-[#1a1a1a] relative flex items-center justify-center overflow-hidden border-b border-[#1a1a1a]">
|
||||
{state.ogImage ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
|
|
@ -908,14 +908,14 @@ export default function MetaTagGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Card Meta Content */}
|
||||
<div className="p-3.5 space-y-1 bg-gray-50 dark:bg-gray-800/80">
|
||||
<span className="text-[11px] uppercase tracking-wider text-gray-500 dark:text-gray-400 font-semibold block truncate">
|
||||
<div className="p-3.5 space-y-1 bg-black">
|
||||
<span className="text-[11px] uppercase tracking-wider text-gray-500 font-semibold block truncate">
|
||||
{state.ogUrl ? new URL(state.ogUrl, "https://example.com").hostname : "EXAMPLE.COM"}
|
||||
</span>
|
||||
<h4 className="text-sm font-bold text-gray-900 dark:text-gray-100 line-clamp-1 leading-snug">
|
||||
<h4 className="text-sm font-bold text-gray-200 line-clamp-1 leading-snug">
|
||||
{state.ogTitle || state.title || "Your Open Graph Title"}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-300 line-clamp-2 leading-relaxed">
|
||||
<p className="text-xs text-gray-600 line-clamp-2 leading-relaxed">
|
||||
{state.ogDescription || state.description || "Your Open Graph description will appear here on social networks."}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -925,8 +925,8 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
{/* Tab 4: Twitter / X Card Preview */}
|
||||
{previewTab === "twitter" && (
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 dark:border-gray-800 pb-2">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 shadow-xs space-y-3">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||
Twitter / X Card Preview
|
||||
</span>
|
||||
|
|
@ -935,8 +935,8 @@ export default function MetaTagGeneratorPage() {
|
|||
|
||||
{state.twitterCard === "summary_large_image" ? (
|
||||
/* Large Image Card */
|
||||
<div className="rounded-2xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-black overflow-hidden shadow-sm">
|
||||
<div className="aspect-[2/1] w-full bg-gray-100 dark:bg-gray-900 relative flex items-center justify-center overflow-hidden border-b border-gray-200 dark:border-gray-800">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] overflow-hidden ">
|
||||
<div className="aspect-[2/1] w-full bg-[#111] relative flex items-center justify-center overflow-hidden border-b border-[#1a1a1a]">
|
||||
{state.twitterImage || state.ogImage ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
|
|
@ -953,22 +953,22 @@ export default function MetaTagGeneratorPage() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3 space-y-0.5 bg-white dark:bg-black">
|
||||
<span className="text-[11px] text-gray-500 dark:text-gray-400 block truncate">
|
||||
<div className="p-3 space-y-0.5 bg-[#0a0a0a]">
|
||||
<span className="text-[11px] text-gray-500 block truncate">
|
||||
{state.canonicalUrl ? new URL(state.canonicalUrl, "https://example.com").hostname : "example.com"}
|
||||
</span>
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100 line-clamp-1">
|
||||
<h4 className="text-sm font-semibold text-gray-200 line-clamp-1">
|
||||
{state.twitterTitle || state.title || "Twitter Card Title"}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
<p className="text-xs text-gray-600 line-clamp-2">
|
||||
{state.twitterDescription || state.description || "Twitter card description summary..."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Summary Small Thumbnail Card */
|
||||
<div className="flex rounded-2xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-black overflow-hidden shadow-sm">
|
||||
<div className="w-28 h-28 bg-gray-100 dark:bg-gray-900 shrink-0 flex items-center justify-center border-r border-gray-200 dark:border-gray-800 overflow-hidden">
|
||||
<div className="flex border border-[#1a1a1a] bg-[#0a0a0a] overflow-hidden ">
|
||||
<div className="w-28 h-28 bg-[#111] shrink-0 flex items-center justify-center border-r border-[#1a1a1a] overflow-hidden">
|
||||
{state.twitterImage || state.ogImage ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
|
|
@ -984,13 +984,13 @@ export default function MetaTagGeneratorPage() {
|
|||
)}
|
||||
</div>
|
||||
<div className="p-2.5 flex-1 flex flex-col justify-center space-y-0.5 overflow-hidden">
|
||||
<span className="text-[10px] text-gray-500 dark:text-gray-400 truncate">
|
||||
<span className="text-[10px] text-gray-500 truncate">
|
||||
{state.canonicalUrl ? new URL(state.canonicalUrl, "https://example.com").hostname : "example.com"}
|
||||
</span>
|
||||
<h4 className="text-xs font-semibold text-gray-900 dark:text-gray-100 line-clamp-1">
|
||||
<h4 className="text-xs font-semibold text-gray-200 line-clamp-1">
|
||||
{state.twitterTitle || state.title || "Twitter Card Title"}
|
||||
</h4>
|
||||
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
<p className="text-[11px] text-gray-600 line-clamp-2">
|
||||
{state.twitterDescription || state.description || "Twitter card description..."}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -252,10 +252,10 @@ export default function NumberBaseConverterPage() {
|
|||
>
|
||||
<div className="space-y-8">
|
||||
{/* Input Configuration Box */}
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50 sm:p-6 space-y-4">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-4 sm:p-6 space-y-4">
|
||||
{/* Base Selector */}
|
||||
<div>
|
||||
<label className="text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300 block mb-2">
|
||||
<label className="text-xs font-bold uppercase tracking-wider text-gray-400 block mb-2">
|
||||
Input Base:
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
|
|
@ -264,10 +264,10 @@ export default function NumberBaseConverterPage() {
|
|||
key={baseOpt.value}
|
||||
type="button"
|
||||
onClick={() => setInputBase(baseOpt.value)}
|
||||
className={`flex items-center justify-center gap-2 rounded-lg py-2.5 px-3 text-sm font-semibold border transition-all ${
|
||||
className={`flex items-center justify-center gap-2 py-2.5 px-3 text-sm font-semibold border transition-all ${
|
||||
inputBase === baseOpt.value
|
||||
? "border-blue-600 bg-blue-600 text-white shadow-sm"
|
||||
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
? "border-blue-600 bg-blue-600 text-white "
|
||||
: "border-[#1a1a1a] bg-[#0a0a0a] text-gray-400 hover:bg-[#111]"
|
||||
}`}
|
||||
>
|
||||
<span>{baseOpt.label}</span>
|
||||
|
|
@ -279,14 +279,14 @@ export default function NumberBaseConverterPage() {
|
|||
{/* Number Input Field */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label htmlFor="num-input" className="text-xs font-semibold text-gray-600 dark:text-gray-400">
|
||||
<label htmlFor="num-input" className="text-xs font-semibold text-gray-600">
|
||||
Number value (supports BigInt & arbitrary precision):
|
||||
</label>
|
||||
{inputVal && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInputVal("")}
|
||||
className="text-xs text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400"
|
||||
className="text-xs text-gray-500 hover:text-red-400"
|
||||
>
|
||||
Clear input
|
||||
</button>
|
||||
|
|
@ -300,17 +300,17 @@ export default function NumberBaseConverterPage() {
|
|||
value={inputVal}
|
||||
onChange={(e) => setInputVal(e.target.value)}
|
||||
placeholder={`Enter a ${BASE_OPTIONS.find((b) => b.value === inputBase)?.radixName} number...`}
|
||||
className={`w-full p-3.5 font-mono text-base sm:text-lg rounded-lg border bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:outline-none ${
|
||||
className={`w-full p-3.5 font-mono text-base sm:text-lg border bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:outline-none ${
|
||||
parseResult.error
|
||||
? "border-red-400 focus:ring-red-400 dark:border-red-600"
|
||||
: "border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
? "border-red-400 focus:ring-red-400"
|
||||
: "border-[#1a1a1a] focus:ring-blue-900"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error Message banner */}
|
||||
{parseResult.error && (
|
||||
<p className="mt-2 text-xs font-medium text-red-600 dark:text-red-400 flex items-center gap-1.5">
|
||||
<p className="mt-2 text-xs font-medium text-red-400 flex items-center gap-1.5">
|
||||
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
|
@ -321,7 +321,7 @@ export default function NumberBaseConverterPage() {
|
|||
|
||||
{/* Preset Buttons */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 mr-1">Presets:</span>
|
||||
<span className="text-xs font-medium text-gray-500 mr-1">Presets:</span>
|
||||
{PRESETS.map((p) => (
|
||||
<button
|
||||
key={p.label}
|
||||
|
|
@ -330,7 +330,7 @@ export default function NumberBaseConverterPage() {
|
|||
setInputBase(p.base as BaseType);
|
||||
setInputVal(p.value);
|
||||
}}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111] transition-colors"
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
|
|
@ -338,13 +338,13 @@ export default function NumberBaseConverterPage() {
|
|||
</div>
|
||||
|
||||
{/* Display Options / Toggles */}
|
||||
<div className="flex flex-wrap items-center gap-4 pt-3 border-t border-gray-200 dark:border-gray-800 text-xs text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center gap-4 pt-3 border-t border-[#1a1a1a] text-xs text-gray-400">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includePrefixes}
|
||||
onChange={(e) => setIncludePrefixes(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Include prefixes (0b, 0o, 0x)</span>
|
||||
</label>
|
||||
|
|
@ -354,7 +354,7 @@ export default function NumberBaseConverterPage() {
|
|||
type="checkbox"
|
||||
checked={hexUppercase}
|
||||
onChange={(e) => setHexUppercase(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Uppercase HEX</span>
|
||||
</label>
|
||||
|
|
@ -364,7 +364,7 @@ export default function NumberBaseConverterPage() {
|
|||
type="checkbox"
|
||||
checked={enableGrouping}
|
||||
onChange={(e) => setEnableGrouping(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Format with digit separators</span>
|
||||
</label>
|
||||
|
|
@ -376,111 +376,111 @@ export default function NumberBaseConverterPage() {
|
|||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{/* Decimal Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-blue-200 bg-blue-50/40 p-4 dark:border-blue-900/40 dark:bg-blue-950/20">
|
||||
<div className="flex flex-col justify-between border border-blue-900 bg-[#0a0a1a]/40 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-blue-500" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-blue-900 dark:text-blue-300">
|
||||
<span className="inline-flex h-2 w-2 rounded-none bg-[#0a0a1a]0" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-blue-900">
|
||||
Decimal (Base 10)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(conversions.dec.raw, "copy-dec")}
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded bg-white dark:bg-gray-800 border border-blue-200 dark:border-blue-900 text-blue-700 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors"
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 bg-[#0a0a0a] border border-blue-900 text-blue-400 hover:bg-[#0a0a1a] transition-colors"
|
||||
>
|
||||
{copiedKey === "copy-dec" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-mono text-lg font-bold text-gray-900 dark:text-white break-all">
|
||||
<div className="font-mono text-lg font-bold text-gray-200 break-all">
|
||||
{conversions.dec.formatted}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-2 border-t border-blue-100 dark:border-blue-900/40 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="mt-3 pt-2 border-t border-blue-100 text-xs text-gray-500">
|
||||
Standard base-10 numerical system
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hexadecimal Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-purple-200 bg-purple-50/40 p-4 dark:border-purple-900/40 dark:bg-purple-950/20">
|
||||
<div className="flex flex-col justify-between border border-purple-200 bg-purple-50/40 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-purple-500" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-purple-900 dark:text-purple-300">
|
||||
<span className="inline-flex h-2 w-2 rounded-none bg-purple-500" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-purple-900">
|
||||
Hexadecimal (Base 16)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(conversions.hex.withPrefix, "copy-hex")}
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded bg-white dark:bg-gray-800 border border-purple-200 dark:border-purple-900 text-purple-700 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-900/50 transition-colors"
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 bg-[#0a0a0a] border border-purple-200 text-purple-700 hover:bg-purple-100 transition-colors"
|
||||
>
|
||||
{copiedKey === "copy-hex" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-mono text-lg font-bold text-gray-900 dark:text-white break-all">
|
||||
<div className="font-mono text-lg font-bold text-gray-200 break-all">
|
||||
{conversions.hex.formatted}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-2 border-t border-purple-100 dark:border-purple-900/40 text-xs text-gray-500 dark:text-gray-400 flex items-center justify-between">
|
||||
<div className="mt-3 pt-2 border-t border-purple-100 text-xs text-gray-500 flex items-center justify-between">
|
||||
<span>Prefix: 0x</span>
|
||||
<span>{conversions.byteCount} Byte{conversions.byteCount > 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Binary Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-emerald-200 bg-emerald-50/40 p-4 dark:border-emerald-900/40 dark:bg-emerald-950/20">
|
||||
<div className="flex flex-col justify-between border border-emerald-200 bg-emerald-50/40 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-emerald-500" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-emerald-900 dark:text-emerald-300">
|
||||
<span className="inline-flex h-2 w-2 rounded-none bg-emerald-500" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-emerald-900">
|
||||
Binary (Base 2)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(conversions.bin.withPrefix, "copy-bin")}
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded bg-white dark:bg-gray-800 border border-emerald-200 dark:border-emerald-900 text-emerald-700 dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors"
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 bg-[#0a0a0a] border border-emerald-200 text-emerald-700 hover:bg-emerald-100 transition-colors"
|
||||
>
|
||||
{copiedKey === "copy-bin" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-mono text-base sm:text-lg font-bold text-gray-900 dark:text-white break-all leading-relaxed">
|
||||
<div className="font-mono text-base sm:text-lg font-bold text-gray-200 break-all leading-relaxed">
|
||||
{conversions.bin.formatted}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-2 border-t border-emerald-100 dark:border-emerald-900/40 text-xs text-gray-500 dark:text-gray-400 flex items-center justify-between">
|
||||
<div className="mt-3 pt-2 border-t border-emerald-100 text-xs text-gray-500 flex items-center justify-between">
|
||||
<span>Prefix: 0b</span>
|
||||
<span>{conversions.bitLength} Bit{conversions.bitLength > 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Octal Card */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-amber-200 bg-amber-50/40 p-4 dark:border-amber-900/40 dark:bg-amber-950/20">
|
||||
<div className="flex flex-col justify-between border border-amber-200 bg-amber-50/40 p-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex h-2 w-2 rounded-full bg-amber-500" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-amber-900 dark:text-amber-300">
|
||||
<span className="inline-flex h-2 w-2 rounded-none bg-amber-500" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-amber-900">
|
||||
Octal (Base 8)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(conversions.oct.withPrefix, "copy-oct")}
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 rounded bg-white dark:bg-gray-800 border border-amber-200 dark:border-amber-900 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/50 transition-colors"
|
||||
className="flex items-center gap-1 text-xs font-medium px-2 py-1 bg-[#0a0a0a] border border-amber-200 text-amber-700 hover:bg-amber-100 transition-colors"
|
||||
>
|
||||
{copiedKey === "copy-oct" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="font-mono text-lg font-bold text-gray-900 dark:text-white break-all">
|
||||
<div className="font-mono text-lg font-bold text-gray-200 break-all">
|
||||
{conversions.oct.formatted}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-2 border-t border-amber-100 dark:border-amber-900/40 text-xs text-gray-500 dark:text-gray-400 flex items-center justify-between">
|
||||
<div className="mt-3 pt-2 border-t border-amber-100 text-xs text-gray-500 flex items-center justify-between">
|
||||
<span>Prefix: 0o</span>
|
||||
<span>Digits: 0-7</span>
|
||||
</div>
|
||||
|
|
@ -490,29 +490,29 @@ export default function NumberBaseConverterPage() {
|
|||
{/* Custom Base Converter & Bit Level Details */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
|
||||
{/* Custom Base Section */}
|
||||
<div className="lg:col-span-6 rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-gray-900 space-y-4">
|
||||
<div className="lg:col-span-6 border border-[#1a1a1a] bg-[#0a0a0a] p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-800 dark:text-gray-200">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-300">
|
||||
Custom Base (Base {customBase})
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(conversions.custom, "copy-custom")}
|
||||
className="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 font-medium"
|
||||
className="text-xs text-blue-600 hover:text-blue-400 font-medium"
|
||||
>
|
||||
{copiedKey === "copy-custom" ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Radix:</span>
|
||||
<span className="text-xs text-gray-500">Radix:</span>
|
||||
<input
|
||||
type="range"
|
||||
min="2"
|
||||
max="36"
|
||||
value={customBase}
|
||||
onChange={(e) => setCustomBase(Number(e.target.value))}
|
||||
className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
className="flex-1 h-2 bg-[#1a1a1a] appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -520,33 +520,33 @@ export default function NumberBaseConverterPage() {
|
|||
max="36"
|
||||
value={customBase}
|
||||
onChange={(e) => setCustomBase(Math.min(Math.max(Number(e.target.value), 2), 36))}
|
||||
className="w-14 p-1.5 text-center font-mono text-xs rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||
className="w-14 p-1.5 text-center font-mono text-xs border border-[#1a1a1a] bg-[#0a0a0a]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="font-mono text-base font-bold text-gray-900 dark:text-white break-all p-3 rounded-lg bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700">
|
||||
<div className="font-mono text-base font-bold text-gray-200 break-all p-3 bg-black border border-[#1a1a1a]">
|
||||
{conversions.custom || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bit Information & ASCII */}
|
||||
<div className="lg:col-span-6 rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-gray-900 space-y-3">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-800 dark:text-gray-200">
|
||||
<div className="lg:col-span-6 border border-[#1a1a1a] bg-[#0a0a0a] p-5 space-y-3">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-300">
|
||||
Bit-Level Representation
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2 text-xs font-mono">
|
||||
<div className="flex items-center justify-between p-2 rounded bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-gray-500 dark:text-gray-400">8-bit (Byte):</span>
|
||||
<span className="text-gray-900 dark:text-white font-bold">{conversions.pad8.slice(-8)}</span>
|
||||
<div className="flex items-center justify-between p-2 bg-black border border-[#1a1a1a]">
|
||||
<span className="text-gray-500">8-bit (Byte):</span>
|
||||
<span className="text-gray-200 font-bold">{conversions.pad8.slice(-8)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 rounded bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-gray-500 dark:text-gray-400">16-bit (Word):</span>
|
||||
<span className="text-gray-900 dark:text-white font-bold">{formatBinary(conversions.pad16.slice(-16), 4)}</span>
|
||||
<div className="flex items-center justify-between p-2 bg-black border border-[#1a1a1a]">
|
||||
<span className="text-gray-500">16-bit (Word):</span>
|
||||
<span className="text-gray-200 font-bold">{formatBinary(conversions.pad16.slice(-16), 4)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-2 rounded bg-gray-50 dark:bg-gray-800/60 border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-gray-500 dark:text-gray-400">ASCII / Character:</span>
|
||||
<span className="text-blue-600 dark:text-blue-400 font-bold">
|
||||
<div className="flex items-center justify-between p-2 bg-black border border-[#1a1a1a]">
|
||||
<span className="text-gray-500">ASCII / Character:</span>
|
||||
<span className="text-blue-600 font-bold">
|
||||
{conversions.asciiChar !== null ? `'${conversions.asciiChar}'` : "Non-printable / Out of ASCII range"}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -557,27 +557,27 @@ export default function NumberBaseConverterPage() {
|
|||
) : null}
|
||||
|
||||
{/* Quick Reference Table (0 to 15) */}
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-800 dark:bg-gray-900/50">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-white mb-3">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-5">
|
||||
<h3 className="text-sm font-bold text-gray-200 mb-3">
|
||||
Quick Reference Table (Values 0 - 15)
|
||||
</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs font-mono">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 uppercase">
|
||||
<tr className="border-b border-[#1a1a1a] text-gray-500 uppercase">
|
||||
<th className="pb-2 pr-4">Decimal (10)</th>
|
||||
<th className="pb-2 pr-4">Binary (2)</th>
|
||||
<th className="pb-2 pr-4">Octal (8)</th>
|
||||
<th className="pb-2">Hexadecimal (16)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800 text-gray-800 dark:text-gray-200">
|
||||
<tbody className="divide-y divide-gray-100 text-gray-300">
|
||||
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].map((val) => (
|
||||
<tr key={val} className="hover:bg-white dark:hover:bg-gray-800/50">
|
||||
<tr key={val} className="hover:bg-[#0a0a0a]">
|
||||
<td className="py-1.5 pr-4 font-semibold">{val}</td>
|
||||
<td className="py-1.5 pr-4">{val.toString(2).padStart(4, "0")}</td>
|
||||
<td className="py-1.5 pr-4">{val.toString(8)}</td>
|
||||
<td className="py-1.5 font-semibold text-purple-600 dark:text-purple-400">
|
||||
<td className="py-1.5 font-semibold text-purple-600">
|
||||
0x{val.toString(16).toUpperCase()}
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function calculateStrength(password: string, poolSize: number): StrengthInfo {
|
|||
return {
|
||||
score: 0,
|
||||
label: "Very Weak",
|
||||
color: "bg-red-500",
|
||||
color: "bg-[#1a0a0a]0",
|
||||
barWidth: "10%",
|
||||
entropy: 0,
|
||||
crackTime: "Instant",
|
||||
|
|
@ -55,7 +55,7 @@ function calculateStrength(password: string, poolSize: number): StrengthInfo {
|
|||
return {
|
||||
score: 1,
|
||||
label: "Weak",
|
||||
color: "bg-red-500",
|
||||
color: "bg-[#1a0a0a]0",
|
||||
barWidth: "25%",
|
||||
entropy,
|
||||
crackTime: "< 1 second",
|
||||
|
|
@ -64,7 +64,7 @@ function calculateStrength(password: string, poolSize: number): StrengthInfo {
|
|||
return {
|
||||
score: 2,
|
||||
label: "Fair",
|
||||
color: "bg-yellow-500",
|
||||
color: "bg-[#1a1a0a]0",
|
||||
barWidth: "50%",
|
||||
entropy,
|
||||
crackTime: "Several hours to days",
|
||||
|
|
@ -73,7 +73,7 @@ function calculateStrength(password: string, poolSize: number): StrengthInfo {
|
|||
return {
|
||||
score: 3,
|
||||
label: "Strong",
|
||||
color: "bg-green-500",
|
||||
color: "bg-[#0a1a0a]0",
|
||||
barWidth: "75%",
|
||||
entropy,
|
||||
crackTime: "Thousands of years",
|
||||
|
|
@ -215,13 +215,13 @@ export default function PasswordGeneratorPage() {
|
|||
>
|
||||
<div className="space-y-6">
|
||||
{/* Generated Password Display Card */}
|
||||
<div className="relative rounded-xl border border-gray-200 bg-gray-50/70 p-5 dark:border-gray-700/80 dark:bg-gray-800/50">
|
||||
<div className="relative border border-[#1a1a1a] bg-black/70 p-5">
|
||||
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<div className="w-full min-w-0 flex-1 overflow-x-auto py-1">
|
||||
<span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-gray-500">
|
||||
Generated Password
|
||||
</span>
|
||||
<p className="select-all break-all font-mono text-xl font-bold tracking-wider text-gray-900 dark:text-white sm:text-2xl">
|
||||
<p className="select-all break-all font-mono text-xl font-bold tracking-wider text-gray-200 sm:text-2xl">
|
||||
{password || (
|
||||
<span className="text-sm font-normal text-red-500">
|
||||
Select at least one character set below
|
||||
|
|
@ -236,7 +236,7 @@ export default function PasswordGeneratorPage() {
|
|||
onClick={generatePassword}
|
||||
disabled={!atLeastOneSelected}
|
||||
title="Regenerate password"
|
||||
className="flex items-center gap-1.5 rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-sm font-medium text-gray-700 shadow-sm transition-colors hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
className="flex items-center gap-1.5 border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2.5 text-sm font-medium text-gray-400 transition-colors hover:bg-black disabled:opacity-50"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -253,7 +253,7 @@ export default function PasswordGeneratorPage() {
|
|||
type="button"
|
||||
onClick={handleCopy}
|
||||
disabled={!password}
|
||||
className={`flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium shadow-sm transition-all disabled:opacity-50 ${
|
||||
className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium transition-all disabled:opacity-50 ${
|
||||
copied
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-blue-600 text-white hover:bg-blue-700 active:scale-95"
|
||||
|
|
@ -284,19 +284,19 @@ export default function PasswordGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Strength Bar */}
|
||||
<div className="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="mt-4 pt-3 border-t border-[#1a1a1a]">
|
||||
<div className="mb-1.5 flex items-center justify-between text-xs">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium text-gray-600">
|
||||
Security Strength:{" "}
|
||||
<strong className="text-gray-900 dark:text-white font-semibold">
|
||||
<strong className="text-gray-200 font-semibold">
|
||||
{strength.label}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
<span className="text-gray-500">
|
||||
~{strength.entropy} bits of entropy ({strength.crackTime})
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div className="h-2 w-full overflow-hidden rounded-none bg-[#1a1a1a]">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${strength.color}`}
|
||||
style={{ width: strength.barWidth }}
|
||||
|
|
@ -306,8 +306,8 @@ export default function PasswordGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Configuration Controls */}
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800/40">
|
||||
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5">
|
||||
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Password Settings
|
||||
</h3>
|
||||
|
||||
|
|
@ -315,8 +315,8 @@ export default function PasswordGeneratorPage() {
|
|||
{/* Length Slider & Presets */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Password Length: <span className="font-bold text-blue-600 dark:text-blue-400">{length}</span> characters
|
||||
<label className="text-sm font-medium text-gray-400">
|
||||
Password Length: <span className="font-bold text-blue-600">{length}</span> characters
|
||||
</label>
|
||||
<div className="flex gap-1">
|
||||
{lengthPresets.map((preset) => (
|
||||
|
|
@ -327,7 +327,7 @@ export default function PasswordGeneratorPage() {
|
|||
className={`rounded px-2 py-0.5 text-xs font-medium transition-colors ${
|
||||
length === preset
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
: "bg-[#111] text-gray-400 hover:bg-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
{preset}
|
||||
|
|
@ -343,7 +343,7 @@ export default function PasswordGeneratorPage() {
|
|||
max="128"
|
||||
value={length}
|
||||
onChange={(e) => setLength(parseInt(e.target.value))}
|
||||
className="h-2.5 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-600 dark:bg-gray-700"
|
||||
className="h-2.5 w-full cursor-pointer appearance-none bg-[#1a1a1a] accent-blue-600"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
|
|
@ -354,76 +354,76 @@ export default function PasswordGeneratorPage() {
|
|||
const val = Math.min(Math.max(8, parseInt(e.target.value) || 8), 128);
|
||||
setLength(val);
|
||||
}}
|
||||
className="w-18 rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 text-center text-sm font-medium text-gray-900 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||||
className="w-18 border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 text-center text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Character Set Checkboxes */}
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4 pt-2">
|
||||
<label className="flex cursor-pointer items-center gap-2.5 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
<label className="flex cursor-pointer items-center gap-2.5 border border-[#1a1a1a] p-3 transition-colors hover:bg-black">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useUppercase}
|
||||
onChange={(e) => setUseUppercase(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<div className="text-sm font-medium text-gray-200">
|
||||
Uppercase
|
||||
</div>
|
||||
<div className="font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="font-mono text-xs text-gray-500">
|
||||
A-Z
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-2.5 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
<label className="flex cursor-pointer items-center gap-2.5 border border-[#1a1a1a] p-3 transition-colors hover:bg-black">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useLowercase}
|
||||
onChange={(e) => setUseLowercase(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<div className="text-sm font-medium text-gray-200">
|
||||
Lowercase
|
||||
</div>
|
||||
<div className="font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="font-mono text-xs text-gray-500">
|
||||
a-z
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-2.5 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
<label className="flex cursor-pointer items-center gap-2.5 border border-[#1a1a1a] p-3 transition-colors hover:bg-black">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useDigits}
|
||||
onChange={(e) => setUseDigits(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<div className="text-sm font-medium text-gray-200">
|
||||
Digits
|
||||
</div>
|
||||
<div className="font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="font-mono text-xs text-gray-500">
|
||||
0-9
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-2.5 rounded-lg border border-gray-200 p-3 transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800">
|
||||
<label className="flex cursor-pointer items-center gap-2.5 border border-[#1a1a1a] p-3 transition-colors hover:bg-black">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useSymbols}
|
||||
onChange={(e) => setUseSymbols(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<div className="text-sm font-medium text-gray-200">
|
||||
Symbols
|
||||
</div>
|
||||
<div className="font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="font-mono text-xs text-gray-500">
|
||||
!@#$%^&*...
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -432,12 +432,12 @@ export default function PasswordGeneratorPage() {
|
|||
|
||||
{/* Ambiguous filter */}
|
||||
<div className="pt-1">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={excludeAmbiguous}
|
||||
onChange={(e) => setExcludeAmbiguous(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Exclude ambiguous characters (e.g. <code>i, l, 1, I, o, 0, O</code>)</span>
|
||||
</label>
|
||||
|
|
@ -447,23 +447,23 @@ export default function PasswordGeneratorPage() {
|
|||
|
||||
{/* History of Generated Passwords */}
|
||||
{history.length > 1 && (
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/80 dark:bg-gray-800/40">
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-400">
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-600">
|
||||
Recent Passwords (Session History)
|
||||
</h3>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
{history.map((pwd, idx) => (
|
||||
<div
|
||||
key={`${pwd}-${idx}`}
|
||||
className="flex items-center justify-between py-2 text-xs"
|
||||
>
|
||||
<span className="truncate font-mono text-gray-800 dark:text-gray-200">
|
||||
<span className="truncate font-mono text-gray-300">
|
||||
{pwd}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopyHistory(pwd, idx)}
|
||||
className="ml-2 shrink-0 rounded bg-white px-2 py-1 font-medium text-gray-700 shadow-xs hover:bg-gray-100 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
className="ml-2 shrink-0 bg-[#0a0a0a] px-2 py-1 font-medium text-gray-400 shadow-xs hover:bg-[#111]"
|
||||
>
|
||||
{historyCopiedIndex === idx ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
|
|
@ -474,21 +474,21 @@ export default function PasswordGeneratorPage() {
|
|||
)}
|
||||
|
||||
{/* Information Grid */}
|
||||
<div className="grid grid-cols-1 gap-4 pt-2 text-xs text-gray-500 dark:text-gray-400 sm:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3.5 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">CSPRNG Powered</h4>
|
||||
<div className="grid grid-cols-1 gap-4 pt-2 text-xs text-gray-500 sm:grid-cols-3">
|
||||
<div className=" border border-gray-100 bg-black p-3.5">
|
||||
<h4 className="font-semibold text-gray-300">CSPRNG Powered</h4>
|
||||
<p className="mt-1">
|
||||
Uses <code>crypto.getRandomValues()</code> for cryptographically secure pseudo-random number generation directly in browser.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3.5 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">Zero Transmission</h4>
|
||||
<div className=" border border-gray-100 bg-black p-3.5">
|
||||
<h4 className="font-semibold text-gray-300">Zero Transmission</h4>
|
||||
<p className="mt-1">
|
||||
Passwords are never transmitted across a network, cached on a server, or stored persistently.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3.5 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">Guaranteed Distribution</h4>
|
||||
<div className=" border border-gray-100 bg-black p-3.5">
|
||||
<h4 className="font-semibold text-gray-300">Guaranteed Distribution</h4>
|
||||
<p className="mt-1">
|
||||
Ensures every active character category is represented and evenly distributed via Fisher-Yates shuffle.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -228,10 +228,10 @@ export default function PlaceholderImagePage() {
|
|||
{/* Preset Sizes Bar */}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Popular Presets
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Click to apply size</span>
|
||||
<span className="text-xs text-gray-500">Click to apply size</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{PRESET_SIZES.map((preset) => {
|
||||
|
|
@ -241,10 +241,10 @@ export default function PlaceholderImagePage() {
|
|||
key={preset.name}
|
||||
type="button"
|
||||
onClick={() => applyPreset(preset)}
|
||||
className={`cursor-pointer rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
|
||||
className={`cursor-pointer px-3 py-1.5 text-xs font-medium transition-all ${
|
||||
isSelected
|
||||
? "bg-blue-600 text-white shadow-sm ring-2 ring-blue-500/30"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
? "bg-blue-600 text-white ring-2 ring-blue-500/30"
|
||||
: "bg-[#111] text-gray-400 hover:bg-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
<span className="font-semibold">{preset.name}</span>
|
||||
|
|
@ -262,14 +262,14 @@ export default function PlaceholderImagePage() {
|
|||
{/* Controls Column (7 Cols) */}
|
||||
<div className="space-y-5 lg:col-span-6">
|
||||
{/* Dimensions */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4.5 shadow-sm dark:border-gray-700 dark:bg-gray-800 space-y-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-4.5 space-y-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Dimensions (Pixels)
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor={widthId} className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label htmlFor={widthId} className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Width (px)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -280,12 +280,12 @@ export default function PlaceholderImagePage() {
|
|||
step="1"
|
||||
value={width}
|
||||
onChange={(e) => setWidth(Math.max(10, Math.min(4000, parseInt(e.target.value) || 10)))}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 font-mono text-sm text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={heightId} className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label htmlFor={heightId} className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Height (px)
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -296,22 +296,22 @@ export default function PlaceholderImagePage() {
|
|||
step="1"
|
||||
value={height}
|
||||
onChange={(e) => setHeight(Math.max(10, Math.min(4000, parseInt(e.target.value) || 10)))}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 font-mono text-sm text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Colors & Palette Swatches */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4.5 shadow-sm dark:border-gray-700 dark:bg-gray-800 space-y-4">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-4.5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Colors & Theme
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSwapColors}
|
||||
className="flex items-center gap-1 text-xs text-blue-600 hover:underline dark:text-blue-400 font-medium cursor-pointer"
|
||||
className="flex items-center gap-1 text-xs text-blue-600 hover:underline font-medium cursor-pointer"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
|
||||
|
|
@ -323,7 +323,7 @@ export default function PlaceholderImagePage() {
|
|||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Background Color */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Background Color
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -331,21 +331,21 @@ export default function PlaceholderImagePage() {
|
|||
type="color"
|
||||
value={bgColor}
|
||||
onChange={(e) => setBgColor(e.target.value)}
|
||||
className="h-9 w-10 cursor-pointer rounded border border-gray-300 bg-transparent p-0.5 dark:border-gray-600"
|
||||
className="h-9 w-10 cursor-pointer border border-[#1a1a1a] bg-transparent p-0.5"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={bgColor}
|
||||
onChange={(e) => setBgColor(e.target.value)}
|
||||
maxLength={7}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 font-mono text-xs text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100 uppercase"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 font-mono text-xs text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20 uppercase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Text Color */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Text Color
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -353,14 +353,14 @@ export default function PlaceholderImagePage() {
|
|||
type="color"
|
||||
value={textColor}
|
||||
onChange={(e) => setTextColor(e.target.value)}
|
||||
className="h-9 w-10 cursor-pointer rounded border border-gray-300 bg-transparent p-0.5 dark:border-gray-600"
|
||||
className="h-9 w-10 cursor-pointer border border-[#1a1a1a] bg-transparent p-0.5"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={textColor}
|
||||
onChange={(e) => setTextColor(e.target.value)}
|
||||
maxLength={7}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 font-mono text-xs text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100 uppercase"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 font-mono text-xs text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20 uppercase"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -368,7 +368,7 @@ export default function PlaceholderImagePage() {
|
|||
|
||||
{/* Palette swatches */}
|
||||
<div>
|
||||
<span className="mb-1.5 block text-[11px] font-medium text-gray-500 dark:text-gray-400">
|
||||
<span className="mb-1.5 block text-[11px] font-medium text-gray-500">
|
||||
Quick Palettes
|
||||
</span>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
|
|
@ -380,20 +380,20 @@ export default function PlaceholderImagePage() {
|
|||
setBgColor(palette.bg);
|
||||
setTextColor(palette.text);
|
||||
}}
|
||||
className="flex cursor-pointer items-center justify-between rounded-lg border border-gray-200 p-1.5 text-[11px] transition-all hover:border-blue-400 hover:shadow-xs dark:border-gray-700"
|
||||
className="flex cursor-pointer items-center justify-between border border-[#1a1a1a] p-1.5 text-[11px] transition-all hover:border-blue-400 hover:shadow-xs"
|
||||
title={palette.name}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="h-4 w-4 rounded-full border border-gray-300 dark:border-gray-600"
|
||||
className="h-4 w-4 rounded-none border border-[#1a1a1a]"
|
||||
style={{ backgroundColor: palette.bg }}
|
||||
/>
|
||||
<span
|
||||
className="h-3 w-3 rounded-full border border-gray-300 dark:border-gray-600 -ml-2"
|
||||
className="h-3 w-3 rounded-none border border-[#1a1a1a] -ml-2"
|
||||
style={{ backgroundColor: palette.text }}
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate text-[10px] text-gray-600 dark:text-gray-300">
|
||||
<span className="truncate text-[10px] text-gray-600">
|
||||
{palette.name.split(" ")[0]}
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -403,13 +403,13 @@ export default function PlaceholderImagePage() {
|
|||
</div>
|
||||
|
||||
{/* Text & Typography */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4.5 shadow-sm dark:border-gray-700 dark:bg-gray-800 space-y-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-4.5 space-y-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Custom Text & Typography
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<label htmlFor={textId} className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label htmlFor={textId} className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Label Text <span className="text-[11px] text-gray-400">(leave blank for W×H, use {"{w}"} and {"{h}"} for dynamic dimensions)</span>
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -418,20 +418,20 @@ export default function PlaceholderImagePage() {
|
|||
value={customText}
|
||||
onChange={(e) => setCustomText(e.target.value)}
|
||||
placeholder={`${width} × ${height}`}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 font-mono text-sm text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Font settings */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Font Family
|
||||
</label>
|
||||
<select
|
||||
value={fontFamily}
|
||||
onChange={(e) => setFontFamily(e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-gray-900 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 text-xs text-gray-200 focus:border-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="sans-serif">Sans-Serif</option>
|
||||
<option value="monospace">Monospace</option>
|
||||
|
|
@ -440,13 +440,13 @@ export default function PlaceholderImagePage() {
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Font Weight
|
||||
</label>
|
||||
<select
|
||||
value={fontWeight}
|
||||
onChange={(e) => setFontWeight(e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-gray-900 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 text-xs text-gray-200 focus:border-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="400">Normal (400)</option>
|
||||
<option value="600">Semibold (600)</option>
|
||||
|
|
@ -456,12 +456,12 @@ export default function PlaceholderImagePage() {
|
|||
</div>
|
||||
|
||||
<div className="col-span-2 sm:col-span-1 flex items-end pb-1">
|
||||
<label className="inline-flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-300 cursor-pointer select-none">
|
||||
<label className="inline-flex items-center gap-2 text-xs font-medium text-gray-400 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showBorder}
|
||||
onChange={(e) => setShowBorder(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Dashed Border</span>
|
||||
</label>
|
||||
|
|
@ -471,15 +471,15 @@ export default function PlaceholderImagePage() {
|
|||
{/* Font Size Selector */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label htmlFor={fontSizeId} className="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
Font Size: <span className="font-semibold text-gray-900 dark:text-gray-100">{calculatedFontSize}px</span>
|
||||
<label htmlFor={fontSizeId} className="text-xs font-medium text-gray-600">
|
||||
Font Size: <span className="font-semibold text-gray-200">{calculatedFontSize}px</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center gap-1.5 text-xs text-blue-600 dark:text-blue-400 cursor-pointer select-none">
|
||||
<label className="inline-flex items-center gap-1.5 text-xs text-blue-600 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isAutoFontSize}
|
||||
onChange={(e) => setIsAutoFontSize(e.target.checked)}
|
||||
className="h-3.5 w-3.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-3.5 w-3.5 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Auto calculate</span>
|
||||
</label>
|
||||
|
|
@ -494,9 +494,9 @@ export default function PlaceholderImagePage() {
|
|||
max="140"
|
||||
value={manualFontSize}
|
||||
onChange={(e) => setManualFontSize(parseInt(e.target.value) || 10)}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-600 dark:bg-gray-700"
|
||||
className="h-2 w-full cursor-pointer appearance-none bg-[#1a1a1a] accent-blue-600"
|
||||
/>
|
||||
<span className="w-8 text-right font-mono text-xs text-gray-700 dark:text-gray-300">
|
||||
<span className="w-8 text-right font-mono text-xs text-gray-400">
|
||||
{manualFontSize}px
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -506,22 +506,22 @@ export default function PlaceholderImagePage() {
|
|||
</div>
|
||||
|
||||
{/* Canvas Preview & Export Actions Column (6 Cols) */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800 lg:col-span-6 space-y-5">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-5 lg:col-span-6 space-y-5">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Live Canvas Preview
|
||||
</h3>
|
||||
<span className="font-mono text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="font-mono text-xs text-gray-500">
|
||||
{width} × {height} px
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Responsive Canvas Container with Checkerboard Background */}
|
||||
<div className="relative flex min-h-[260px] max-h-[360px] w-full items-center justify-center overflow-hidden rounded-xl border border-gray-200 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:12px_12px] p-3 dark:border-gray-700 dark:bg-[radial-gradient(#374151_1px,transparent_1px)] shadow-inner">
|
||||
<div className="relative flex min-h-[260px] max-h-[360px] w-full items-center justify-center overflow-hidden border border-[#1a1a1a] bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:12px_12px] p-3(#374151_1px,transparent_1px)] shadow-inner">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="max-h-full max-w-full rounded-md shadow-md object-contain transition-transform"
|
||||
className="max-h-full max-w-full object-contain transition-transform"
|
||||
style={{
|
||||
aspectRatio: `${width} / ${height}`,
|
||||
}}
|
||||
|
|
@ -535,7 +535,7 @@ export default function PlaceholderImagePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleDownloadPng}
|
||||
className="flex cursor-pointer items-center justify-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 active:bg-blue-800"
|
||||
className="flex cursor-pointer items-center justify-center gap-1.5 bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
|
|
@ -546,7 +546,7 @@ export default function PlaceholderImagePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleDownloadSvg}
|
||||
className="flex cursor-pointer items-center justify-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-indigo-700 active:bg-indigo-800"
|
||||
className="flex cursor-pointer items-center justify-center gap-1.5 bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-indigo-700 active:bg-indigo-800"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
|
|
@ -560,7 +560,7 @@ export default function PlaceholderImagePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopyDataUri}
|
||||
className="cursor-pointer rounded-lg bg-gray-100 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="cursor-pointer bg-[#111] px-3 py-2 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "dataUri" ? "✓ Copied Data URI!" : "Copy PNG Data URI"}
|
||||
</button>
|
||||
|
|
@ -568,7 +568,7 @@ export default function PlaceholderImagePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopySvgDataUri}
|
||||
className="cursor-pointer rounded-lg bg-gray-100 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="cursor-pointer bg-[#111] px-3 py-2 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "svgDataUri" ? "✓ Copied SVG URI!" : "Copy SVG Data URI"}
|
||||
</button>
|
||||
|
|
@ -576,7 +576,7 @@ export default function PlaceholderImagePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopySvgCode}
|
||||
className="cursor-pointer rounded-lg bg-gray-100 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="cursor-pointer bg-[#111] px-3 py-2 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "svgCode" ? "✓ Copied SVG Code!" : "Copy SVG Markup"}
|
||||
</button>
|
||||
|
|
@ -584,7 +584,7 @@ export default function PlaceholderImagePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleCopyImgTag}
|
||||
className="cursor-pointer rounded-lg bg-gray-100 px-3 py-2 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center justify-center gap-1"
|
||||
className="cursor-pointer bg-[#111] px-3 py-2 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{copiedKey === "imgTag" ? "✓ Copied HTML Tag!" : "Copy <img> Tag"}
|
||||
</button>
|
||||
|
|
@ -594,21 +594,21 @@ export default function PlaceholderImagePage() {
|
|||
</div>
|
||||
|
||||
{/* Informative Grid */}
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 border-t border-gray-200 pt-6 text-xs text-gray-600 dark:border-gray-800 dark:text-gray-400 md:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">Standard Aspect Ratios</h4>
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 border-t border-[#1a1a1a] pt-6 text-xs text-gray-600 md:grid-cols-3">
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">Standard Aspect Ratios</h4>
|
||||
<p>
|
||||
Includes pre-configured dimensions for Open Graph social cards (1200×630), leaderboard ad banners (728×90), and avatars (64×64).
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">Crisp Vector SVG & PNG</h4>
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">Crisp Vector SVG & PNG</h4>
|
||||
<p>
|
||||
Export as scalable XML vector graphics for lightweight mockups or rasterized PNG files for maximum software compatibility.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">Client-Side Canvas Rendering</h4>
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">Client-Side Canvas Rendering</h4>
|
||||
<p>
|
||||
Zero network requests required. Placeholders render instantly inside your browser canvas with zero server overhead.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -168,21 +168,21 @@ export default function PxToRemPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Base Font Size Configuration Bar */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4.5 shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-4.5 ">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
htmlFor={baseId}
|
||||
className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
className="text-xs font-semibold uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Root Base Font Size
|
||||
</label>
|
||||
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-semibold text-blue-800 dark:bg-blue-900/60 dark:text-blue-300">
|
||||
<span className="rounded-none bg-[#0a0a1a] px-2 py-0.5 text-[11px] font-semibold text-blue-400">
|
||||
1rem = {baseSize}px
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Standard browser default is 16px. Change to 10px if using the 62.5% font-size technique.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -196,9 +196,9 @@ export default function PxToRemPage() {
|
|||
max="100"
|
||||
value={baseSize}
|
||||
onChange={(e) => handleBaseChange(parseInt(e.target.value) || 16)}
|
||||
className="w-20 rounded-lg border border-gray-300 bg-white px-3 py-1.5 font-mono text-sm font-semibold text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100"
|
||||
className="w-20 border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-1.5 font-mono text-sm font-semibold text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">px</span>
|
||||
<span className="text-sm font-medium text-gray-600">px</span>
|
||||
</div>
|
||||
|
||||
{/* Preset buttons */}
|
||||
|
|
@ -208,10 +208,10 @@ export default function PxToRemPage() {
|
|||
key={preset.value}
|
||||
type="button"
|
||||
onClick={() => handleBaseChange(preset.value)}
|
||||
className={`cursor-pointer rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
||||
className={`cursor-pointer px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
||||
baseSize === preset.value
|
||||
? "bg-blue-600 text-white shadow-xs"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
: "bg-[#111] text-gray-400 hover:bg-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
{preset.value}px
|
||||
|
|
@ -225,8 +225,8 @@ export default function PxToRemPage() {
|
|||
{/* Two-Way Interactive Live Converter */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12">
|
||||
{/* Main Converter Box (7 Cols) */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800 lg:col-span-7 space-y-5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 lg:col-span-7 space-y-5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Live Two-Way Converter
|
||||
</h3>
|
||||
|
||||
|
|
@ -235,7 +235,7 @@ export default function PxToRemPage() {
|
|||
{/* PX Input */}
|
||||
<div className="sm:col-span-5 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={pxInputId} className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={pxInputId} className="text-xs font-semibold text-gray-400">
|
||||
Pixels (PX)
|
||||
</label>
|
||||
<span className="text-[11px] text-gray-400">Fixed</span>
|
||||
|
|
@ -248,7 +248,7 @@ export default function PxToRemPage() {
|
|||
value={pxValue}
|
||||
onChange={(e) => handlePxChange(e.target.value)}
|
||||
placeholder="24"
|
||||
className="w-full rounded-lg border border-gray-300 bg-white p-3 pr-10 font-mono text-lg font-bold text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900/70 dark:text-gray-100 shadow-inner"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] p-3 pr-10 font-mono text-lg font-bold text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20 shadow-inner"
|
||||
/>
|
||||
<span className="pointer-events-none absolute right-3 top-3.5 font-mono text-sm font-semibold text-gray-400">
|
||||
px
|
||||
|
|
@ -258,7 +258,7 @@ export default function PxToRemPage() {
|
|||
|
||||
{/* Arrow Indicator */}
|
||||
<div className="sm:col-span-1 flex justify-center text-gray-400 pt-1">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-none bg-[#111] text-gray-600">
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
|
|
@ -268,10 +268,10 @@ export default function PxToRemPage() {
|
|||
{/* REM Input */}
|
||||
<div className="sm:col-span-5 space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={remInputId} className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={remInputId} className="text-xs font-semibold text-gray-400">
|
||||
Root Em (REM)
|
||||
</label>
|
||||
<span className="text-[11px] text-blue-600 dark:text-blue-400 font-medium">Relative</span>
|
||||
<span className="text-[11px] text-blue-600 font-medium">Relative</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<input
|
||||
|
|
@ -281,7 +281,7 @@ export default function PxToRemPage() {
|
|||
value={remValue}
|
||||
onChange={(e) => handleRemChange(e.target.value)}
|
||||
placeholder="1.5"
|
||||
className="w-full rounded-lg border border-gray-300 bg-white p-3 pr-12 font-mono text-lg font-bold text-blue-600 dark:text-blue-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-900/70 shadow-inner"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] p-3 pr-12 font-mono text-lg font-bold text-blue-600 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20 shadow-inner"
|
||||
/>
|
||||
<span className="pointer-events-none absolute right-3 top-3.5 font-mono text-sm font-semibold text-gray-400">
|
||||
rem
|
||||
|
|
@ -292,31 +292,31 @@ export default function PxToRemPage() {
|
|||
|
||||
{/* Quick Multi-Unit Equivalents Cards */}
|
||||
<div>
|
||||
<span className="block text-[11px] font-medium text-gray-500 dark:text-gray-400 mb-2">
|
||||
<span className="block text-[11px] font-medium text-gray-500 mb-2">
|
||||
Equivalent Dimensions for {pxValue || 0}px:
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-4">
|
||||
<div className="rounded-lg bg-gray-50 p-2 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<div className=" bg-black p-2 border border-[#1a1a1a]">
|
||||
<span className="block text-[10px] text-gray-500 uppercase font-medium">REM</span>
|
||||
<span className="font-mono text-xs font-semibold text-blue-600 dark:text-blue-400">
|
||||
<span className="font-mono text-xs font-semibold text-blue-600">
|
||||
{remValue || "0"}rem
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-lg bg-gray-50 p-2 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<div className=" bg-black p-2 border border-[#1a1a1a]">
|
||||
<span className="block text-[10px] text-gray-500 uppercase font-medium">EM</span>
|
||||
<span className="font-mono text-xs font-semibold text-gray-800 dark:text-gray-200">
|
||||
<span className="font-mono text-xs font-semibold text-gray-300">
|
||||
{remValue || "0"}em
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-lg bg-gray-50 p-2 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<div className=" bg-black p-2 border border-[#1a1a1a]">
|
||||
<span className="block text-[10px] text-gray-500 uppercase font-medium">Percent (%)</span>
|
||||
<span className="font-mono text-xs font-semibold text-gray-800 dark:text-gray-200">
|
||||
<span className="font-mono text-xs font-semibold text-gray-300">
|
||||
{percentageValue}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-lg bg-gray-50 p-2 dark:bg-gray-900/60 border border-gray-200 dark:border-gray-700/60">
|
||||
<div className=" bg-black p-2 border border-[#1a1a1a]">
|
||||
<span className="block text-[10px] text-gray-500 uppercase font-medium">Points (pt)</span>
|
||||
<span className="font-mono text-xs font-semibold text-gray-800 dark:text-gray-200">
|
||||
<span className="font-mono text-xs font-semibold text-gray-300">
|
||||
{ptValue}pt
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -324,15 +324,15 @@ export default function PxToRemPage() {
|
|||
</div>
|
||||
|
||||
{/* CSS Declaration Generator */}
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/70 p-3.5 dark:border-gray-700/60 dark:bg-gray-900/40 space-y-2">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-3.5 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<span className="text-xs font-semibold text-gray-400">
|
||||
CSS Declaration Snippet
|
||||
</span>
|
||||
<select
|
||||
value={selectedProperty}
|
||||
onChange={(e) => setSelectedProperty(e.target.value)}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300"
|
||||
>
|
||||
<option value="font-size">font-size</option>
|
||||
<option value="padding">padding</option>
|
||||
|
|
@ -344,12 +344,12 @@ export default function PxToRemPage() {
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 rounded-md border border-gray-300 bg-white px-3 py-2 font-mono text-xs text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100">
|
||||
<div className="flex items-center justify-between gap-2 border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 font-mono text-xs text-gray-200">
|
||||
<span className="truncate">{cssDeclaration}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(cssDeclaration, "css-decl")}
|
||||
className="cursor-pointer shrink-0 rounded bg-blue-50 px-2.5 py-1 text-xs font-semibold text-blue-600 hover:bg-blue-100 dark:bg-blue-900/40 dark:text-blue-300 dark:hover:bg-blue-900/70 transition-colors"
|
||||
className="cursor-pointer shrink-0 bg-[#0a0a1a] px-2.5 py-1 text-xs font-semibold text-blue-600 hover:bg-[#0a0a1a] transition-colors"
|
||||
>
|
||||
{copiedKey === "css-decl" ? "✓ Copied!" : "Copy CSS"}
|
||||
</button>
|
||||
|
|
@ -358,39 +358,39 @@ export default function PxToRemPage() {
|
|||
</div>
|
||||
|
||||
{/* Formula & How It Works Card (5 Cols) */}
|
||||
<div className="flex flex-col justify-between rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800 lg:col-span-5 space-y-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-col justify-between border border-[#1a1a1a] bg-[#0a0a0a] p-5 lg:col-span-5 space-y-4">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Calculation Formula
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3 font-mono text-xs">
|
||||
<div className="rounded-lg bg-blue-50/70 p-3 dark:bg-blue-950/40 border border-blue-200 dark:border-blue-900/50">
|
||||
<span className="block text-[11px] font-sans font-semibold text-blue-800 dark:text-blue-300 mb-1">
|
||||
<div className=" bg-[#0a0a1a]/70 p-3 border border-blue-900">
|
||||
<span className="block text-[11px] font-sans font-semibold text-blue-400 mb-1">
|
||||
Pixels to Rem:
|
||||
</span>
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
<p className="text-gray-200">
|
||||
rem = px ÷ baseFont ({baseSize})
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
|
||||
<p className="mt-1 text-[11px] text-gray-500">
|
||||
Example: {pxValue || 24}px ÷ {baseSize} = {remValue || 1.5}rem
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-indigo-50/70 p-3 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-900/50">
|
||||
<span className="block text-[11px] font-sans font-semibold text-indigo-800 dark:text-indigo-300 mb-1">
|
||||
<div className=" bg-indigo-50/70 p-3 border border-indigo-200">
|
||||
<span className="block text-[11px] font-sans font-semibold text-indigo-800 mb-1">
|
||||
Rem to Pixels:
|
||||
</span>
|
||||
<p className="text-gray-900 dark:text-gray-100">
|
||||
<p className="text-gray-200">
|
||||
px = rem × baseFont ({baseSize})
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
|
||||
<p className="mt-1 text-[11px] text-gray-500">
|
||||
Example: {remValue || 1.5}rem × {baseSize} = {pxValue || 24}px
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3 text-xs text-gray-600 dark:border-gray-700 dark:bg-gray-900/50 dark:text-gray-400">
|
||||
<p className="font-semibold text-gray-800 dark:text-gray-200 mb-1">💡 Why use REM over PX?</p>
|
||||
<div className=" border border-[#1a1a1a] bg-black p-3 text-xs text-gray-600">
|
||||
<p className="font-semibold text-gray-300 mb-1">💡 Why use REM over PX?</p>
|
||||
<p>
|
||||
REM units scale proportionally when users adjust their operating system or browser font size settings, ensuring accessibility (WCAG) compliance.
|
||||
</p>
|
||||
|
|
@ -399,13 +399,13 @@ export default function PxToRemPage() {
|
|||
</div>
|
||||
|
||||
{/* Quick Reference Table */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800 space-y-4">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Quick Reference Table (Base: {baseSize}px)
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-gray-500">
|
||||
Click any row to load into converter or copy individual values.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -417,14 +417,14 @@ export default function PxToRemPage() {
|
|||
placeholder="Search px or rem..."
|
||||
value={tableSearch}
|
||||
onChange={(e) => setTableSearch(e.target.value)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-xs text-gray-900 placeholder:text-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-1.5 text-xs text-gray-200 placeholder:text-gray-400 focus:border-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<table className="w-full text-left text-xs text-gray-700 dark:text-gray-300">
|
||||
<thead className="bg-gray-50 text-[11px] uppercase tracking-wider text-gray-500 dark:bg-gray-900 dark:text-gray-400">
|
||||
<div className="overflow-x-auto border border-[#1a1a1a]">
|
||||
<table className="w-full text-left text-xs text-gray-400">
|
||||
<thead className="bg-black text-[11px] uppercase tracking-wider text-gray-500">
|
||||
<tr>
|
||||
<th className="px-4 py-2.5 font-semibold">Pixels (px)</th>
|
||||
<th className="px-4 py-2.5 font-semibold">Root Em (rem)</th>
|
||||
|
|
@ -433,7 +433,7 @@ export default function PxToRemPage() {
|
|||
<th className="px-4 py-2.5 text-right font-semibold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody className="divide-y divide-[#1a1a1a]">
|
||||
{filteredTableValues.map((px) => {
|
||||
const rem = formatNumber(px / baseSize);
|
||||
const isSelected = px.toString() === pxValue;
|
||||
|
|
@ -443,22 +443,22 @@ export default function PxToRemPage() {
|
|||
<tr
|
||||
key={px}
|
||||
onClick={() => handlePxChange(px.toString())}
|
||||
className={`cursor-pointer transition-colors hover:bg-blue-50/50 dark:hover:bg-blue-950/20 ${
|
||||
isSelected ? "bg-blue-50/80 dark:bg-blue-950/40 font-semibold" : ""
|
||||
className={`cursor-pointer transition-colors hover:bg-[#0a0a1a]/50 ${
|
||||
isSelected ? "bg-[#0a0a1a]/80 font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-mono">
|
||||
<span className="rounded bg-gray-100 px-2 py-0.5 font-bold text-gray-900 dark:bg-gray-700 dark:text-gray-100">
|
||||
<span className="rounded bg-[#111] px-2 py-0.5 font-bold text-gray-200">
|
||||
{px}px
|
||||
</span>
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-mono text-blue-600 dark:text-blue-400 font-bold">
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-mono text-blue-600 font-bold">
|
||||
{rem}rem
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-mono text-gray-600 dark:text-gray-400">
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-mono text-gray-600">
|
||||
{rem}em
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-mono text-emerald-600 dark:text-emerald-400">
|
||||
<td className="whitespace-nowrap px-4 py-2.5 font-mono text-emerald-600">
|
||||
{tailwindClass}
|
||||
</td>
|
||||
<td className="whitespace-nowrap px-4 py-2.5 text-right">
|
||||
|
|
@ -468,7 +468,7 @@ export default function PxToRemPage() {
|
|||
e.stopPropagation();
|
||||
copyToClipboard(`${rem}rem`, `row-${px}`);
|
||||
}}
|
||||
className="cursor-pointer rounded bg-gray-100 px-2.5 py-1 text-[11px] font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
className="cursor-pointer bg-[#111] px-2.5 py-1 text-[11px] font-medium text-gray-400 hover:bg-[#1a1a1a] transition-colors"
|
||||
>
|
||||
{copiedKey === `row-${px}` ? "✓ Copied" : "Copy REM"}
|
||||
</button>
|
||||
|
|
@ -482,24 +482,24 @@ export default function PxToRemPage() {
|
|||
</div>
|
||||
|
||||
{/* Bulk Converter Section */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-700 dark:bg-gray-800 space-y-4">
|
||||
<div className=" border border-[#1a1a1a] bg-[#0a0a0a] p-5 space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Bulk Pixel Converter
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-gray-500">
|
||||
Paste multiple pixel values (separated by newlines or commas) to convert all at once.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Bulk Format Selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Format:</span>
|
||||
<span className="text-xs text-gray-500">Format:</span>
|
||||
<select
|
||||
value={bulkFormat}
|
||||
onChange={(e) => setBulkFormat(e.target.value as "rem-list" | "mapping" | "css-vars" | "tailwind")}
|
||||
className="rounded-lg border border-gray-300 bg-white px-2.5 py-1.5 text-xs text-gray-800 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2.5 py-1.5 text-xs text-gray-300"
|
||||
>
|
||||
<option value="rem-list">Rem Values (0.5rem)</option>
|
||||
<option value="mapping">Mapping (8px = 0.5rem)</option>
|
||||
|
|
@ -513,13 +513,13 @@ export default function PxToRemPage() {
|
|||
{/* Input Box */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label htmlFor={bulkInputId} className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={bulkInputId} className="text-xs font-medium text-gray-400">
|
||||
Input Pixel Values (one per line)
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBulkInput("8\n12\n16\n20\n24\n32\n40\n48\n64\n80\n96\n128")}
|
||||
className="text-xs text-blue-600 hover:underline dark:text-blue-400 font-medium cursor-pointer"
|
||||
className="text-xs text-blue-600 hover:underline font-medium cursor-pointer"
|
||||
>
|
||||
Load Sample Scale
|
||||
</button>
|
||||
|
|
@ -530,21 +530,21 @@ export default function PxToRemPage() {
|
|||
value={bulkInput}
|
||||
onChange={(e) => setBulkInput(e.target.value)}
|
||||
placeholder="Paste numbers like: 12px 16px 24px 32px"
|
||||
className="w-full rounded-lg border border-gray-300 bg-white p-3 font-mono text-xs text-gray-900 shadow-inner focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-900/60 dark:text-gray-100 leading-relaxed"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-xs text-gray-200 shadow-inner focus:border-blue-500 focus:outline-none leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Output Box */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="text-xs font-medium text-gray-400">
|
||||
Converted Output ({parsedBulkRows.length} items)
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(getBulkOutputString(), "bulk-output")}
|
||||
disabled={!getBulkOutputString()}
|
||||
className="cursor-pointer rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-40 transition-colors"
|
||||
className="cursor-pointer bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-40 transition-colors"
|
||||
>
|
||||
{copiedKey === "bulk-output" ? "✓ Copied All!" : "Copy All"}
|
||||
</button>
|
||||
|
|
@ -554,28 +554,28 @@ export default function PxToRemPage() {
|
|||
rows={7}
|
||||
value={getBulkOutputString()}
|
||||
placeholder="Bulk conversion output will appear here..."
|
||||
className="w-full rounded-lg border border-gray-300 bg-gray-50 p-3 font-mono text-xs text-gray-900 shadow-inner focus:outline-none dark:border-gray-700 dark:bg-gray-900/80 dark:text-gray-100 leading-relaxed select-all"
|
||||
className="w-full border border-[#1a1a1a] bg-black p-3 font-mono text-xs text-gray-200 shadow-inner focus:outline-none leading-relaxed select-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Informational Guidelines */}
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 border-t border-gray-200 pt-6 text-xs text-gray-600 dark:border-gray-800 dark:text-gray-400 md:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">REM vs EM Units</h4>
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 border-t border-[#1a1a1a] pt-6 text-xs text-gray-600 md:grid-cols-3">
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">REM vs EM Units</h4>
|
||||
<p>
|
||||
<code>rem</code> units are calculated relative to the root <code><html></code> element font size, avoiding compound scaling issues encountered with nested <code>em</code> elements.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">The 62.5% Trick (10px Base)</h4>
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">The 62.5% Trick (10px Base)</h4>
|
||||
<p>
|
||||
Setting <code>html { font-size: 62.5%; }</code> makes 1rem = 10px (1.6rem = 16px, 2.4rem = 24px) while preserving user browser zoom scaling.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200 mb-1">Instant Client Calculation</h4>
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h4 className="font-semibold text-gray-300 mb-1">Instant Client Calculation</h4>
|
||||
<p>
|
||||
Computations happen instantly in your browser on each keystroke without sending queries over the network.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export default function QrCodePage() {
|
|||
<div className="mb-2 flex items-center justify-between">
|
||||
<label
|
||||
htmlFor="qr-input"
|
||||
className="text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||
className="text-xs font-semibold uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
Content / URL
|
||||
</label>
|
||||
|
|
@ -132,7 +132,7 @@ export default function QrCodePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setInputText("")}
|
||||
className="text-xs text-red-600 hover:text-red-700 dark:text-red-400"
|
||||
className="text-xs text-red-400 hover:text-red-400"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -145,18 +145,18 @@ export default function QrCodePage() {
|
|||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
placeholder="Enter a URL, text, or select a template below..."
|
||||
className="w-full rounded-lg border border-gray-300 bg-white p-3 font-mono text-sm text-gray-900 shadow-inner focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-sm text-gray-200 shadow-inner focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
|
||||
{/* Presets */}
|
||||
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Templates:</span>
|
||||
<span className="text-xs text-gray-500">Templates:</span>
|
||||
{presetTemplates.map((tpl) => (
|
||||
<button
|
||||
key={tpl.label}
|
||||
type="button"
|
||||
onClick={() => setInputText(tpl.value)}
|
||||
className="rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className=" bg-[#111] px-2 py-1 text-xs font-medium text-gray-400 hover:bg-[#1a1a1a]"
|
||||
>
|
||||
{tpl.label}
|
||||
</button>
|
||||
|
|
@ -165,21 +165,21 @@ export default function QrCodePage() {
|
|||
</div>
|
||||
|
||||
{/* Configuration Options */}
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<h3 className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Customization Options
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{/* Size Selector */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Export Resolution
|
||||
</label>
|
||||
<select
|
||||
value={size}
|
||||
onChange={(e) => setSize(e.target.value as SizeOption)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
>
|
||||
<option value="small">{SIZE_MAP.small.label}</option>
|
||||
<option value="medium">{SIZE_MAP.medium.label}</option>
|
||||
|
|
@ -189,13 +189,13 @@ export default function QrCodePage() {
|
|||
|
||||
{/* Error Correction */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Error Correction Level
|
||||
</label>
|
||||
<select
|
||||
value={errorCorrection}
|
||||
onChange={(e) => setErrorCorrection(e.target.value as ErrorCorrectionLevel)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
>
|
||||
<option value="L">L - {ERROR_CORRECTION_DESCRIPTIONS.L}</option>
|
||||
<option value="M">M - {ERROR_CORRECTION_DESCRIPTIONS.M}</option>
|
||||
|
|
@ -206,7 +206,7 @@ export default function QrCodePage() {
|
|||
|
||||
{/* Margin / Quiet Zone */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Margin / Border: {margin} module{margin > 1 ? "s" : ""}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -216,9 +216,9 @@ export default function QrCodePage() {
|
|||
max="6"
|
||||
value={margin}
|
||||
onChange={(e) => setMargin(parseInt(e.target.value))}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 accent-blue-600 dark:bg-gray-700"
|
||||
className="h-2 w-full cursor-pointer appearance-none bg-[#1a1a1a] accent-blue-600"
|
||||
/>
|
||||
<span className="w-6 text-center text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="w-6 text-center text-xs font-medium text-gray-400">
|
||||
{margin}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -226,7 +226,7 @@ export default function QrCodePage() {
|
|||
|
||||
{/* Colors */}
|
||||
<div>
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
<label className="mb-1 block text-xs font-medium text-gray-600">
|
||||
Colors (Foreground / Background)
|
||||
</label>
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
|
|
@ -235,10 +235,10 @@ export default function QrCodePage() {
|
|||
type="color"
|
||||
value={fgColor}
|
||||
onChange={(e) => setFgColor(e.target.value)}
|
||||
className="h-8 w-8 cursor-pointer rounded border border-gray-300 bg-transparent p-0.5 dark:border-gray-600"
|
||||
className="h-8 w-8 cursor-pointer border border-[#1a1a1a] bg-transparent p-0.5"
|
||||
title="Foreground color"
|
||||
/>
|
||||
<span className="font-mono text-xs text-gray-700 dark:text-gray-300">
|
||||
<span className="font-mono text-xs text-gray-400">
|
||||
{fgColor}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -248,10 +248,10 @@ export default function QrCodePage() {
|
|||
type="color"
|
||||
value={bgColor}
|
||||
onChange={(e) => setBgColor(e.target.value)}
|
||||
className="h-8 w-8 cursor-pointer rounded border border-gray-300 bg-transparent p-0.5 dark:border-gray-600"
|
||||
className="h-8 w-8 cursor-pointer border border-[#1a1a1a] bg-transparent p-0.5"
|
||||
title="Background color"
|
||||
/>
|
||||
<span className="font-mono text-xs text-gray-700 dark:text-gray-300">
|
||||
<span className="font-mono text-xs text-gray-400">
|
||||
{bgColor}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -263,7 +263,7 @@ export default function QrCodePage() {
|
|||
setFgColor("#000000");
|
||||
setBgColor("#ffffff");
|
||||
}}
|
||||
className="text-xs text-blue-600 hover:underline dark:text-blue-400"
|
||||
className="text-xs text-blue-600 hover:underline"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
|
|
@ -275,13 +275,13 @@ export default function QrCodePage() {
|
|||
</div>
|
||||
|
||||
{/* QR Code Preview & Download Column */}
|
||||
<div className="flex flex-col items-center justify-center rounded-xl border border-gray-200 bg-gray-50/50 p-6 dark:border-gray-700/60 dark:bg-gray-800/40 lg:col-span-5">
|
||||
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-col items-center justify-center border border-[#1a1a1a] bg-black/50 p-6 lg:col-span-5">
|
||||
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Live QR Preview
|
||||
</h3>
|
||||
|
||||
{error ? (
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center rounded-lg border border-red-200 bg-red-50 p-4 text-center dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center border border-red-900 bg-[#1a0a0a] p-4 text-center">
|
||||
<svg className="mb-2 h-8 w-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
|
|
@ -290,14 +290,14 @@ export default function QrCodePage() {
|
|||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-xs font-medium text-red-700 dark:text-red-400">{error}</p>
|
||||
<p className="mt-1 text-[11px] text-red-600 dark:text-red-500">
|
||||
<p className="text-xs font-medium text-red-400">{error}</p>
|
||||
<p className="mt-1 text-[11px] text-red-400">
|
||||
Try shortening the text or lowering the error correction level.
|
||||
</p>
|
||||
</div>
|
||||
) : dataUrl ? (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="overflow-hidden rounded-xl border border-gray-200 bg-white p-3 shadow-md dark:border-gray-700 dark:bg-gray-900">
|
||||
<div className="overflow-hidden border border-[#1a1a1a] bg-[#0a0a0a] p-3 ">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={dataUrl}
|
||||
|
|
@ -308,7 +308,7 @@ export default function QrCodePage() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<p className="mt-2 text-center text-xs text-gray-400 dark:text-gray-500">
|
||||
<p className="mt-2 text-center text-xs text-gray-400">
|
||||
{SIZE_MAP[size].px} × {SIZE_MAP[size].px} px • Level {errorCorrection}
|
||||
</p>
|
||||
|
||||
|
|
@ -317,7 +317,7 @@ export default function QrCodePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleDownloadPng}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
className="flex w-full items-center justify-center gap-2 bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 focus:outline-none focus:ring-1 focus:ring-blue-900 focus:ring-offset-2"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -334,14 +334,14 @@ export default function QrCodePage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleDownloadSvg}
|
||||
className="flex-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-800 transition-colors hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
className="flex-1 bg-[#1a1a1a] px-3 py-2 text-xs font-medium text-gray-300 transition-colors hover:bg-[#222]"
|
||||
>
|
||||
Download SVG
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyDataUrl}
|
||||
className="flex-1 rounded-lg bg-gray-200 px-3 py-2 text-xs font-medium text-gray-800 transition-colors hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
className="flex-1 bg-[#1a1a1a] px-3 py-2 text-xs font-medium text-gray-300 transition-colors hover:bg-[#222]"
|
||||
>
|
||||
{copiedDataUrl ? "Copied Data URI!" : "Copy Data URI"}
|
||||
</button>
|
||||
|
|
@ -349,9 +349,9 @@ export default function QrCodePage() {
|
|||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 p-4 text-center dark:border-gray-700">
|
||||
<div className="flex h-64 w-full flex-col items-center justify-center border border-dashed border-[#1a1a1a] p-4 text-center">
|
||||
<span className="text-3xl">📱</span>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Enter text or a URL above to render a live QR Code.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -360,21 +360,21 @@ export default function QrCodePage() {
|
|||
</div>
|
||||
|
||||
{/* Informational Details */}
|
||||
<div className="grid grid-cols-1 gap-4 pt-4 text-xs text-gray-500 dark:text-gray-400 sm:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">Error Correction (Reed-Solomon)</h4>
|
||||
<div className="grid grid-cols-1 gap-4 pt-4 text-xs text-gray-500 sm:grid-cols-3">
|
||||
<div className=" border border-gray-100 bg-black p-3">
|
||||
<h4 className="font-semibold text-gray-300">Error Correction (Reed-Solomon)</h4>
|
||||
<p className="mt-1">
|
||||
Higher error correction allows the QR code to be scanned even if damaged, partially obstructed, or printed on textured materials.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">Universal Smartphone Support</h4>
|
||||
<div className=" border border-gray-100 bg-black p-3">
|
||||
<h4 className="font-semibold text-gray-300">Universal Smartphone Support</h4>
|
||||
<p className="mt-1">
|
||||
Scannable natively with standard iOS and Android camera apps without requiring any third-party scanner software.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">Zero Server Transfer</h4>
|
||||
<div className=" border border-gray-100 bg-black p-3">
|
||||
<h4 className="font-semibold text-gray-300">Zero Server Transfer</h4>
|
||||
<p className="mt-1">
|
||||
Your sensitive Wi-Fi passwords, contact cards, and URLs are encoded entirely in client JavaScript.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -173,12 +173,12 @@ export default function RegexTesterPage() {
|
|||
|
||||
if (item.match.length === 0) {
|
||||
// Zero-length match indicator
|
||||
html += `<span class="bg-amber-400 text-black font-bold px-0.5 rounded text-xs border border-amber-500" title="Zero-width match at index ${item.start}">|</span>`;
|
||||
html += `<span class="bg-amber-400 text-black font-bold px-0.5 text-xs border border-amber-500" title="Zero-width match at index ${item.start}">|</span>`;
|
||||
} else {
|
||||
const colorClass =
|
||||
i % 2 === 0
|
||||
? "bg-yellow-200 dark:bg-yellow-800/70 text-yellow-950 dark:text-yellow-100 border-b-2 border-yellow-500"
|
||||
: "bg-teal-200 dark:bg-teal-800/70 text-teal-950 dark:text-teal-100 border-b-2 border-teal-500";
|
||||
? "bg-yellow-200 text-yellow-950 border-b-2 border-yellow-500"
|
||||
: "bg-teal-200 text-teal-950 border-b-2 border-teal-500";
|
||||
html += `<mark class="${colorClass} px-0.5 rounded-xs font-mono font-medium">${escapeHtml(
|
||||
item.match
|
||||
)}</mark>`;
|
||||
|
|
@ -258,9 +258,9 @@ export default function RegexTesterPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Preset Selector */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 p-3 bg-gray-50 dark:bg-gray-800/60 rounded-xl border border-gray-200 dark:border-gray-700/80">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 p-3 bg-black border border-[#1a1a1a]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500">
|
||||
Presets:
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
|
|
@ -269,7 +269,7 @@ export default function RegexTesterPage() {
|
|||
key={i}
|
||||
type="button"
|
||||
onClick={() => loadPreset(p)}
|
||||
className="px-2.5 py-1 text-xs font-medium bg-white dark:bg-gray-850 hover:bg-blue-50 dark:hover:bg-blue-950/40 text-gray-700 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 border border-gray-200 dark:border-gray-700 rounded-md transition-colors cursor-pointer shadow-xs"
|
||||
className="px-2.5 py-1 text-xs font-medium bg-[#0a0a0a] hover:bg-[#0a0a1a] text-gray-400 hover:text-blue-600 border border-[#1a1a1a] transition-colors cursor-pointer shadow-xs"
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
|
|
@ -283,7 +283,7 @@ export default function RegexTesterPage() {
|
|||
setTestString("");
|
||||
setReplacement("");
|
||||
}}
|
||||
className="text-xs text-rose-600 dark:text-rose-400 hover:underline cursor-pointer"
|
||||
className="text-xs text-rose-600 hover:underline cursor-pointer"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
|
|
@ -293,17 +293,17 @@ export default function RegexTesterPage() {
|
|||
<div className="space-y-3">
|
||||
<label
|
||||
htmlFor="regex-pattern-input"
|
||||
className="text-sm font-semibold text-gray-800 dark:text-gray-200 flex items-center justify-between"
|
||||
className="text-sm font-semibold text-gray-300 flex items-center justify-between"
|
||||
>
|
||||
<span>Regular Expression Pattern</span>
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs font-normal text-gray-500 font-mono">
|
||||
/{pattern}/{flagString}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
|
||||
<div className="relative flex-1 flex items-center">
|
||||
<span className="absolute left-3.5 text-gray-400 dark:text-gray-500 font-mono text-base select-none pointer-events-none">
|
||||
<span className="absolute left-3.5 text-gray-400 font-mono text-base select-none pointer-events-none">
|
||||
/
|
||||
</span>
|
||||
<input
|
||||
|
|
@ -312,16 +312,16 @@ export default function RegexTesterPage() {
|
|||
value={pattern}
|
||||
onChange={(e) => setPattern(e.target.value)}
|
||||
placeholder="Enter regex pattern, e.g. [a-z0-9]+"
|
||||
className="w-full pl-7 pr-7 py-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none transition-shadow"
|
||||
className="w-full pl-7 pr-7 py-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none transition-shadow"
|
||||
/>
|
||||
<span className="absolute right-3.5 text-gray-400 dark:text-gray-500 font-mono text-base select-none pointer-events-none">
|
||||
<span className="absolute right-3.5 text-gray-400 font-mono text-base select-none pointer-events-none">
|
||||
/
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Flags Checkboxes */}
|
||||
<div className="flex items-center gap-2 bg-gray-50 dark:bg-gray-800/80 p-1.5 rounded-lg border border-gray-200 dark:border-gray-700 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400 font-semibold px-1">Flags:</span>
|
||||
<div className="flex items-center gap-2 bg-black p-1.5 border border-[#1a1a1a] text-xs">
|
||||
<span className="text-gray-500 font-semibold px-1">Flags:</span>
|
||||
{[
|
||||
{ key: "g", label: "g", title: "Global (match all)" },
|
||||
{ key: "i", label: "i", title: "Case insensitive" },
|
||||
|
|
@ -337,10 +337,10 @@ export default function RegexTesterPage() {
|
|||
type="button"
|
||||
title={title}
|
||||
onClick={() => toggleFlag(k)}
|
||||
className={`px-2 py-1 rounded font-mono font-semibold transition-colors cursor-pointer ${
|
||||
className={`px-2 py-1 font-mono font-semibold transition-colors cursor-pointer ${
|
||||
active
|
||||
? "bg-blue-600 text-white shadow-xs"
|
||||
: "bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
: "bg-[#1a1a1a] text-gray-400 hover:bg-[#222]"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
|
|
@ -352,7 +352,7 @@ export default function RegexTesterPage() {
|
|||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2.5 p-3 rounded-lg bg-rose-50 dark:bg-rose-950/40 border border-rose-200 dark:border-rose-800 text-rose-800 dark:text-rose-300 text-xs font-mono">
|
||||
<div className="flex items-center gap-2.5 p-3 bg-rose-50 border border-rose-200 text-rose-800 text-xs font-mono">
|
||||
<svg className="w-4 h-4 shrink-0 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
|
@ -366,10 +366,10 @@ export default function RegexTesterPage() {
|
|||
{/* Test String Textarea */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor="regex-test-string" className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor="regex-test-string" className="text-sm font-semibold text-gray-400">
|
||||
Test String
|
||||
</label>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{testString.length} chars
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -379,7 +379,7 @@ export default function RegexTesterPage() {
|
|||
value={testString}
|
||||
onChange={(e) => setTestString(e.target.value)}
|
||||
placeholder="Paste or type text to match against the pattern..."
|
||||
className="w-full p-3.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
className="w-full p-3.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y leading-relaxed shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -387,25 +387,25 @@ export default function RegexTesterPage() {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-sm font-semibold text-gray-400">
|
||||
Live Match Highlight
|
||||
</label>
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-semibold ${
|
||||
className={`px-2 py-0.5 rounded-none text-xs font-semibold ${
|
||||
matches.length > 0
|
||||
? "bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300"
|
||||
: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
? "bg-emerald-100 text-emerald-800"
|
||||
: "bg-[#111] text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{matches.length} {matches.length === 1 ? "match" : "matches"}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">
|
||||
<span className="text-xs text-gray-400">
|
||||
Alternating highlights
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="w-full p-3.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50/70 dark:bg-gray-900/60 text-gray-900 dark:text-gray-100 font-mono text-sm overflow-auto min-h-[190px] max-h-[300px] whitespace-pre-wrap break-words leading-relaxed select-text"
|
||||
className="w-full p-3.5 border border-[#1a1a1a] bg-black/70 text-gray-200 font-mono text-sm overflow-auto min-h-[190px] max-h-[300px] whitespace-pre-wrap break-words leading-relaxed select-text"
|
||||
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -414,7 +414,7 @@ export default function RegexTesterPage() {
|
|||
{/* Detailed Matches & Capture Groups List */}
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||||
<h3 className="text-sm font-bold text-gray-200 flex items-center gap-2">
|
||||
<span>Match Details & Captured Groups</span>
|
||||
<span className="text-xs font-normal text-gray-500">
|
||||
({matches.length} {matches.length === 1 ? "item" : "items"})
|
||||
|
|
@ -423,7 +423,7 @@ export default function RegexTesterPage() {
|
|||
</div>
|
||||
|
||||
{matches.length === 0 ? (
|
||||
<div className="p-6 text-center rounded-xl border border-dashed border-gray-300 dark:border-gray-700 text-gray-500 dark:text-gray-400 text-sm">
|
||||
<div className="p-6 text-center border border-dashed border-[#1a1a1a] text-gray-500 text-sm">
|
||||
{pattern ? "No matches found in the test string." : "Enter a regular expression pattern above to see matches."}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -431,27 +431,27 @@ export default function RegexTesterPage() {
|
|||
{matches.map((item) => (
|
||||
<div
|
||||
key={item.index}
|
||||
className="p-3.5 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/80 shadow-xs space-y-2"
|
||||
className="p-3.5 border border-[#1a1a1a] bg-[#0a0a0a] shadow-xs space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-gray-100 dark:border-gray-700 pb-2">
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-0.5 rounded text-xs font-bold bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300">
|
||||
<span className="px-2 py-0.5 text-xs font-bold bg-[#0a0a1a] text-blue-400">
|
||||
Match #{item.index}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
pos: [{item.start}, {item.end}] ({item.match.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopyMatch(item.match, item.index)}
|
||||
className="text-xs text-gray-500 hover:text-blue-600 dark:hover:text-blue-400 font-medium cursor-pointer"
|
||||
className="text-xs text-gray-500 hover:text-blue-600 font-medium cursor-pointer"
|
||||
>
|
||||
{copiedIndex === item.index ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="font-mono text-xs p-2 rounded bg-gray-50 dark:bg-gray-900/80 text-gray-900 dark:text-gray-100 break-all">
|
||||
<div className="font-mono text-xs p-2 bg-black text-gray-200 break-all">
|
||||
{item.match.length === 0 ? (
|
||||
<span className="italic text-gray-400"><empty match></span>
|
||||
) : (
|
||||
|
|
@ -461,8 +461,8 @@ export default function RegexTesterPage() {
|
|||
|
||||
{/* Groups */}
|
||||
{item.groups.length > 0 && (
|
||||
<div className="space-y-1 pt-1 border-t border-gray-100 dark:border-gray-800 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400 font-medium">
|
||||
<div className="space-y-1 pt-1 border-t border-gray-100 text-xs">
|
||||
<span className="text-gray-500 font-medium">
|
||||
Captured Groups:
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
|
|
@ -471,10 +471,10 @@ export default function RegexTesterPage() {
|
|||
key={gIdx}
|
||||
className="flex items-start gap-2 font-mono text-xs pl-2 border-l-2 border-indigo-400"
|
||||
>
|
||||
<span className="text-indigo-600 dark:text-indigo-400 font-semibold shrink-0">
|
||||
<span className="text-indigo-600 font-semibold shrink-0">
|
||||
Group {gIdx + 1}:
|
||||
</span>
|
||||
<span className="text-gray-800 dark:text-gray-200 break-all">
|
||||
<span className="text-gray-300 break-all">
|
||||
{grp !== "" ? `"${grp}"` : <span className="italic text-gray-400">undefined</span>}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -485,8 +485,8 @@ export default function RegexTesterPage() {
|
|||
|
||||
{/* Named Groups */}
|
||||
{item.namedGroups && Object.keys(item.namedGroups).length > 0 && (
|
||||
<div className="space-y-1 pt-1 border-t border-gray-100 dark:border-gray-800 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400 font-medium">
|
||||
<div className="space-y-1 pt-1 border-t border-gray-100 text-xs">
|
||||
<span className="text-gray-500 font-medium">
|
||||
Named Groups:
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
|
|
@ -495,10 +495,10 @@ export default function RegexTesterPage() {
|
|||
key={name}
|
||||
className="flex items-start gap-2 font-mono text-xs pl-2 border-l-2 border-teal-400"
|
||||
>
|
||||
<span className="text-teal-600 dark:text-teal-400 font-semibold shrink-0">
|
||||
<span className="text-teal-600 font-semibold shrink-0">
|
||||
?<{name}>:
|
||||
</span>
|
||||
<span className="text-gray-800 dark:text-gray-200 break-all">
|
||||
<span className="text-gray-300 break-all">
|
||||
"{val}"
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -513,12 +513,12 @@ export default function RegexTesterPage() {
|
|||
</div>
|
||||
|
||||
{/* Substitution / Replace Section */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-850 p-4 space-y-3">
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="text-sm font-bold text-gray-200">
|
||||
Regex Substitution / Replace
|
||||
</h3>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs text-gray-500">
|
||||
Supports $1, $2 group references
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -527,7 +527,7 @@ export default function RegexTesterPage() {
|
|||
<div className="space-y-1.5">
|
||||
<label
|
||||
htmlFor="regex-replace-input"
|
||||
className="text-xs font-semibold text-gray-700 dark:text-gray-300"
|
||||
className="text-xs font-semibold text-gray-400"
|
||||
>
|
||||
Replacement String
|
||||
</label>
|
||||
|
|
@ -537,25 +537,25 @@ export default function RegexTesterPage() {
|
|||
value={replacement}
|
||||
onChange={(e) => setReplacement(e.target.value)}
|
||||
placeholder="e.g. [$1] or REDACTED"
|
||||
className="w-full p-2.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Result After Replacement
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyReplaced}
|
||||
disabled={!replacedText}
|
||||
className="text-xs text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-40 cursor-pointer font-medium"
|
||||
className="text-xs text-blue-600 hover:underline disabled:opacity-40 cursor-pointer font-medium"
|
||||
>
|
||||
{copiedReplace ? "Copied!" : "Copy Result"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-2.5 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-mono text-xs min-h-[42px] max-h-32 overflow-y-auto whitespace-pre-wrap break-words select-text">
|
||||
<div className="p-2.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-xs min-h-[42px] max-h-32 overflow-y-auto whitespace-pre-wrap break-words select-text">
|
||||
{replacedText}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -335,16 +335,16 @@ export default function SlugGeneratorPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Presets and Actions Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-gray-200 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-3 border-b border-[#1a1a1a]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Presets:</span>
|
||||
<span className="text-xs font-semibold text-gray-500">Presets:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applyPreset("url")}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`px-3 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
activePreset === "url"
|
||||
? "bg-blue-600 text-white shadow-xs"
|
||||
: "bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200"
|
||||
: "bg-[#111] hover:bg-[#1a1a1a] text-gray-400"
|
||||
}`}
|
||||
>
|
||||
URL Slug (my-post-title)
|
||||
|
|
@ -352,10 +352,10 @@ export default function SlugGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => applyPreset("filename")}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`px-3 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
activePreset === "filename"
|
||||
? "bg-blue-600 text-white shadow-xs"
|
||||
: "bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200"
|
||||
: "bg-[#111] hover:bg-[#1a1a1a] text-gray-400"
|
||||
}`}
|
||||
>
|
||||
Filename-Safe (my_file_name)
|
||||
|
|
@ -363,10 +363,10 @@ export default function SlugGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => applyPreset("wordpress")}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`px-3 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
activePreset === "wordpress"
|
||||
? "bg-blue-600 text-white shadow-xs"
|
||||
: "bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200"
|
||||
: "bg-[#111] hover:bg-[#1a1a1a] text-gray-400"
|
||||
}`}
|
||||
>
|
||||
WordPress Style
|
||||
|
|
@ -374,10 +374,10 @@ export default function SlugGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => applyPreset("seo-clean")}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`px-3 py-1.5 text-xs font-semibold transition-all cursor-pointer ${
|
||||
activePreset === "seo-clean"
|
||||
? "bg-blue-600 text-white shadow-xs"
|
||||
: "bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200"
|
||||
: "bg-[#111] hover:bg-[#1a1a1a] text-gray-400"
|
||||
}`}
|
||||
>
|
||||
SEO Clean (<60 + No Stop Words)
|
||||
|
|
@ -388,7 +388,7 @@ export default function SlugGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setInput(SAMPLE_TEXT)}
|
||||
className="px-3 py-1.5 text-xs font-medium bg-blue-50 hover:bg-blue-100 dark:bg-blue-950/50 dark:hover:bg-blue-900/50 text-blue-700 dark:text-blue-300 rounded-lg transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs font-medium bg-[#0a0a1a] hover:bg-[#0a0a1a] text-blue-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Load Sample
|
||||
</button>
|
||||
|
|
@ -396,7 +396,7 @@ export default function SlugGeneratorPage() {
|
|||
type="button"
|
||||
onClick={() => setInput("")}
|
||||
disabled={!input}
|
||||
className="px-3 py-1.5 text-xs font-medium bg-gray-100 hover:bg-rose-50 dark:bg-gray-800 dark:hover:bg-rose-950/30 text-gray-700 dark:text-gray-300 hover:text-rose-600 dark:hover:text-rose-400 disabled:opacity-40 rounded-lg transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs font-medium bg-[#111] hover:bg-rose-50 text-gray-400 hover:text-rose-600 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -404,9 +404,9 @@ export default function SlugGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Configuration Options Card */}
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 bg-gray-50/70 dark:bg-gray-900/50 p-4 space-y-4 shadow-xs">
|
||||
<div className="flex items-center justify-between border-b border-gray-200/80 dark:border-gray-800 pb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-4 space-y-4 shadow-xs">
|
||||
<div className="flex items-center justify-between border-b border-[#1a1a1a]/80 pb-2">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-gray-400">
|
||||
Slug Configuration & Formatting Options
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 font-mono">Real-time update</span>
|
||||
|
|
@ -415,7 +415,7 @@ export default function SlugGeneratorPage() {
|
|||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Separator Selection */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Word Separator
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-1">
|
||||
|
|
@ -432,10 +432,10 @@ export default function SlugGeneratorPage() {
|
|||
setOptions({ ...options, separator: s.val });
|
||||
setActivePreset("custom");
|
||||
}}
|
||||
className={`py-1.5 text-center text-xs font-mono font-bold rounded-md border transition-colors cursor-pointer ${
|
||||
className={`py-1.5 text-center text-xs font-mono font-bold border transition-colors cursor-pointer ${
|
||||
options.separator === s.val
|
||||
? "bg-blue-600 border-blue-600 text-white"
|
||||
: "bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
: "bg-[#0a0a0a] border-[#1a1a1a] text-gray-300 hover:bg-[#111]"
|
||||
}`}
|
||||
title={s.label}
|
||||
>
|
||||
|
|
@ -447,7 +447,7 @@ export default function SlugGeneratorPage() {
|
|||
|
||||
{/* Letter Casing */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Letter Casing
|
||||
</label>
|
||||
<select
|
||||
|
|
@ -459,7 +459,7 @@ export default function SlugGeneratorPage() {
|
|||
});
|
||||
setActivePreset("custom");
|
||||
}}
|
||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
>
|
||||
<option value="lowercase">lowercase (recommended)</option>
|
||||
<option value="uppercase">UPPERCASE</option>
|
||||
|
|
@ -469,7 +469,7 @@ export default function SlugGeneratorPage() {
|
|||
|
||||
{/* Max Length Limit */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label className="text-xs font-semibold text-gray-400">
|
||||
Max Length (characters)
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -484,13 +484,13 @@ export default function SlugGeneratorPage() {
|
|||
setActivePreset("custom");
|
||||
}}
|
||||
placeholder="Unlimited (default)"
|
||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-xs focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-2 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 text-xs focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
{options.maxLength !== null && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOptions({ ...options, maxLength: null })}
|
||||
className="p-2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
className="p-2 text-xs text-gray-400 hover:text-gray-600"
|
||||
title="Remove max length"
|
||||
>
|
||||
✕
|
||||
|
|
@ -501,7 +501,7 @@ export default function SlugGeneratorPage() {
|
|||
|
||||
{/* Quick Toggle Checkboxes */}
|
||||
<div className="space-y-2 pt-1">
|
||||
<label className="flex items-center gap-2 text-xs text-gray-700 dark:text-gray-300 cursor-pointer select-none">
|
||||
<label className="flex items-center gap-2 text-xs text-gray-400 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.transliterateUnicode}
|
||||
|
|
@ -509,12 +509,12 @@ export default function SlugGeneratorPage() {
|
|||
setOptions({ ...options, transliterateUnicode: e.target.checked });
|
||||
setActivePreset("custom");
|
||||
}}
|
||||
className="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-3.5 h-3.5 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Convert Accents (é→e, ñ→n)</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-2 text-xs text-gray-700 dark:text-gray-300 cursor-pointer select-none">
|
||||
<label className="flex items-center gap-2 text-xs text-gray-400 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={options.convertSymbols}
|
||||
|
|
@ -522,7 +522,7 @@ export default function SlugGeneratorPage() {
|
|||
setOptions({ ...options, convertSymbols: e.target.checked });
|
||||
setActivePreset("custom");
|
||||
}}
|
||||
className="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-3.5 h-3.5 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Replace & with "and", @ with "at"</span>
|
||||
</label>
|
||||
|
|
@ -530,7 +530,7 @@ export default function SlugGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Secondary Toggles Row */}
|
||||
<div className="flex flex-wrap items-center gap-6 pt-2 border-t border-gray-200 dark:border-gray-800 text-xs text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center gap-6 pt-2 border-t border-[#1a1a1a] text-xs text-gray-400">
|
||||
<label className="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -539,7 +539,7 @@ export default function SlugGeneratorPage() {
|
|||
setOptions({ ...options, removeSpecialChars: e.target.checked });
|
||||
setActivePreset("custom");
|
||||
}}
|
||||
className="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-3.5 h-3.5 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Remove Special Characters & Emojis</span>
|
||||
</label>
|
||||
|
|
@ -552,7 +552,7 @@ export default function SlugGeneratorPage() {
|
|||
setOptions({ ...options, removeStopWords: e.target.checked });
|
||||
setActivePreset("custom");
|
||||
}}
|
||||
className="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-3.5 h-3.5 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Remove Stop Words ("a", "the", "in", "with")</span>
|
||||
</label>
|
||||
|
|
@ -566,7 +566,7 @@ export default function SlugGeneratorPage() {
|
|||
setOptions({ ...options, smartTruncate: e.target.checked });
|
||||
setActivePreset("custom");
|
||||
}}
|
||||
className="w-3.5 h-3.5 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-3.5 h-3.5 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Smart Truncate (don't split words in half)</span>
|
||||
</label>
|
||||
|
|
@ -579,7 +579,7 @@ export default function SlugGeneratorPage() {
|
|||
{/* Input Area */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={inputId} className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={inputId} className="text-sm font-semibold text-gray-400">
|
||||
Input Text or Multi-line Titles
|
||||
</label>
|
||||
<span className="text-xs text-gray-400 font-mono">
|
||||
|
|
@ -593,7 +593,7 @@ export default function SlugGeneratorPage() {
|
|||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Type or paste titles, post headlines, or phrases here (one per line for bulk conversion)..."
|
||||
className="w-full p-3.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-sans text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y shadow-inner leading-relaxed"
|
||||
className="w-full p-3.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-sans text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y shadow-inner leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -601,10 +601,10 @@ export default function SlugGeneratorPage() {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor={outputId} className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={outputId} className="text-sm font-semibold text-gray-400">
|
||||
Generated Slugs
|
||||
</label>
|
||||
<span className="px-2 py-0.5 rounded-full text-xs font-semibold bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300">
|
||||
<span className="px-2 py-0.5 rounded-none text-xs font-semibold bg-emerald-100 text-emerald-800">
|
||||
{validSlugCount} {validSlugCount === 1 ? "slug" : "slugs"}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -614,7 +614,7 @@ export default function SlugGeneratorPage() {
|
|||
type="button"
|
||||
onClick={handleDownload}
|
||||
disabled={!bulkOutput}
|
||||
className="px-2.5 py-1 bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 disabled:opacity-40 rounded text-xs font-medium transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 disabled:opacity-40 text-xs font-medium transition-colors cursor-pointer"
|
||||
title="Download all slugs as .txt"
|
||||
>
|
||||
Download .txt
|
||||
|
|
@ -623,7 +623,7 @@ export default function SlugGeneratorPage() {
|
|||
type="button"
|
||||
onClick={handleCopyAll}
|
||||
disabled={!bulkOutput}
|
||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copiedAll ? (
|
||||
<>
|
||||
|
|
@ -650,16 +650,16 @@ export default function SlugGeneratorPage() {
|
|||
value={bulkOutput}
|
||||
readOnly
|
||||
placeholder="Generated slugs will appear here in real time..."
|
||||
className="w-full p-3.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/70 text-gray-900 dark:text-gray-100 font-mono text-sm focus:outline-none resize-y select-all shadow-inner leading-relaxed"
|
||||
className="w-full p-3.5 border border-[#1a1a1a] bg-black text-gray-200 font-mono text-sm focus:outline-none resize-y select-all shadow-inner leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live URL Preview Card for the First/Primary Slug */}
|
||||
{slugs.length > 0 && slugs[0] && (
|
||||
<div className="rounded-xl border border-blue-200 bg-blue-50/50 dark:border-blue-900/40 dark:bg-blue-950/20 p-4 space-y-2">
|
||||
<div className=" border border-blue-900 bg-[#0a0a1a]/50 p-4 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-blue-700 dark:text-blue-400 flex items-center gap-1.5">
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-blue-400 flex items-center gap-1.5">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
|
|
@ -668,14 +668,14 @@ export default function SlugGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => handleCopySingle(`https://example.com/blog/${slugs[0]}`, 999)}
|
||||
className="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 font-medium cursor-pointer"
|
||||
className="text-xs text-blue-600 hover:text-blue-400 font-medium cursor-pointer"
|
||||
>
|
||||
{copiedIndex === 999 ? "Copied URL!" : "Copy Full URL"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-2.5 rounded-lg bg-white dark:bg-gray-900 border border-blue-100 dark:border-blue-900/60 font-mono text-xs text-gray-800 dark:text-gray-200 overflow-x-auto whitespace-nowrap">
|
||||
<div className="p-2.5 bg-[#0a0a0a] border border-blue-100 font-mono text-xs text-gray-300 overflow-x-auto whitespace-nowrap">
|
||||
<span className="text-gray-400 select-none">https://example.com/blog/</span>
|
||||
<span className="font-bold text-blue-600 dark:text-blue-400">{slugs[0]}</span>
|
||||
<span className="font-bold text-blue-600">{slugs[0]}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -684,7 +684,7 @@ export default function SlugGeneratorPage() {
|
|||
{lines.length > 1 && (
|
||||
<div className="space-y-3 pt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-bold text-gray-900 dark:text-gray-100">
|
||||
<h3 className="text-sm font-bold text-gray-200">
|
||||
Individual Slugs Breakdown ({lines.length} items)
|
||||
</h3>
|
||||
<span className="text-xs text-gray-400">Click "Copy" on any individual slug</span>
|
||||
|
|
@ -700,14 +700,14 @@ export default function SlugGeneratorPage() {
|
|||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-3 rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-850 hover:border-blue-300 dark:hover:border-blue-700 transition-colors"
|
||||
className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 p-3 border border-[#1a1a1a] bg-[#0a0a0a] hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 truncate" title={rawLine}>
|
||||
<div className="text-xs text-gray-500 truncate" title={rawLine}>
|
||||
<span className="font-semibold text-gray-400 mr-1.5">#{idx + 1}</span>
|
||||
{rawLine}
|
||||
</div>
|
||||
<div className="font-mono text-xs font-semibold text-gray-900 dark:text-gray-100 truncate">
|
||||
<div className="font-mono text-xs font-semibold text-gray-200 truncate">
|
||||
{slugVal || <span className="text-gray-400 italic">No output</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -720,10 +720,10 @@ export default function SlugGeneratorPage() {
|
|||
type="button"
|
||||
onClick={() => handleCopySingle(slugVal, idx)}
|
||||
disabled={!slugVal}
|
||||
className={`px-2.5 py-1 rounded text-xs font-medium transition-colors cursor-pointer ${
|
||||
className={`px-2.5 py-1 text-xs font-medium transition-colors cursor-pointer ${
|
||||
isCopied
|
||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
|
||||
: "bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-750 dark:hover:bg-gray-700 dark:text-gray-200"
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-[#111] hover:bg-[#1a1a1a] text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{isCopied ? "Copied!" : "Copy"}
|
||||
|
|
@ -737,17 +737,17 @@ export default function SlugGeneratorPage() {
|
|||
)}
|
||||
|
||||
{/* SEO & Developer Tips Section */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800 grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div className="p-3.5 bg-gray-50 dark:bg-gray-850/60 rounded-xl border border-gray-200 dark:border-gray-800">
|
||||
<h4 className="font-bold text-gray-900 dark:text-gray-200 mb-1">SEO Best Practices</h4>
|
||||
<div className="mt-8 pt-6 border-t border-[#1a1a1a] grid grid-cols-1 md:grid-cols-3 gap-4 text-xs text-gray-600">
|
||||
<div className="p-3.5 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-bold text-gray-200 mb-1">SEO Best Practices</h4>
|
||||
<p>Keep slugs between 3 to 5 words (<60 chars) and separate words with hyphens for optimal search engine crawling.</p>
|
||||
</div>
|
||||
<div className="p-3.5 bg-gray-50 dark:bg-gray-850/60 rounded-xl border border-gray-200 dark:border-gray-800">
|
||||
<h4 className="font-bold text-gray-900 dark:text-gray-200 mb-1">Accent Transliteration</h4>
|
||||
<div className="p-3.5 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-bold text-gray-200 mb-1">Accent Transliteration</h4>
|
||||
<p>Accented characters from European languages (like é, ü, ç, ñ, ø, å, ß) are mapped to clean ASCII equivalents automatically.</p>
|
||||
</div>
|
||||
<div className="p-3.5 bg-gray-50 dark:bg-gray-850/60 rounded-xl border border-gray-200 dark:border-gray-800">
|
||||
<h4 className="font-bold text-gray-900 dark:text-gray-200 mb-1">Bulk Line Processing</h4>
|
||||
<div className="p-3.5 bg-black border border-[#1a1a1a]">
|
||||
<h4 className="font-bold text-gray-200 mb-1">Bulk Line Processing</h4>
|
||||
<p>Paste multiple titles or list of article headlines at once to generate clean slugs for all items simultaneously.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -601,11 +601,11 @@ export default function SqlFormatterPage() {
|
|||
>
|
||||
<div className="space-y-5">
|
||||
{/* Top Controls Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-4 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={handleFormat}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
|
|
@ -615,7 +615,7 @@ export default function SqlFormatterPage() {
|
|||
|
||||
<button
|
||||
onClick={handleMinify}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-800 transition-colors hover:bg-gray-300 active:scale-95 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#1a1a1a] px-4 py-2 text-sm font-medium text-gray-300 transition-colors hover:bg-[#222] active:scale-95 cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
|
|
@ -625,7 +625,7 @@ export default function SqlFormatterPage() {
|
|||
|
||||
<button
|
||||
onClick={handleLoadSample}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-50 px-3 py-2 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/50 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#0a0a1a] px-3 py-2 text-sm font-medium text-blue-400 transition-colors hover:bg-[#0a0a1a] cursor-pointer"
|
||||
>
|
||||
Load Sample SQL
|
||||
</button>
|
||||
|
|
@ -633,24 +633,24 @@ export default function SqlFormatterPage() {
|
|||
<button
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-rose-950/30 dark:hover:text-rose-400 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#111] px-3 py-2 text-sm font-medium text-gray-400 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-400">
|
||||
{/* Indent Selector */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor={indentSelectId} className="text-xs font-semibold text-gray-500 uppercase dark:text-gray-400">
|
||||
<label htmlFor={indentSelectId} className="text-xs font-semibold text-gray-500 uppercase">
|
||||
Indent:
|
||||
</label>
|
||||
<select
|
||||
id={indentSelectId}
|
||||
value={indentStr}
|
||||
onChange={(e) => setIndentStr(e.target.value)}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value=" ">2 Spaces</option>
|
||||
<option value=" ">4 Spaces</option>
|
||||
|
|
@ -660,14 +660,14 @@ export default function SqlFormatterPage() {
|
|||
|
||||
{/* Keyword Casing */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor={casingSelectId} className="text-xs font-semibold text-gray-500 uppercase dark:text-gray-400">
|
||||
<label htmlFor={casingSelectId} className="text-xs font-semibold text-gray-500 uppercase">
|
||||
Keywords:
|
||||
</label>
|
||||
<select
|
||||
id={casingSelectId}
|
||||
value={keywordCase}
|
||||
onChange={(e) => setKeywordCase(e.target.value as "upper" | "lower" | "preserve")}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value="upper">UPPERCASE</option>
|
||||
<option value="lower">lowercase</option>
|
||||
|
|
@ -676,12 +676,12 @@ export default function SqlFormatterPage() {
|
|||
</div>
|
||||
|
||||
{/* Column Line Break */}
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-xs text-gray-600 select-none hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-xs text-gray-600 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={breakCommas}
|
||||
onChange={(e) => setBreakCommas(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Column per line</span>
|
||||
</label>
|
||||
|
|
@ -690,9 +690,9 @@ export default function SqlFormatterPage() {
|
|||
|
||||
{/* Status Notification Banner */}
|
||||
{status.type === "success" && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600 dark:text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
|
|
@ -701,7 +701,7 @@ export default function SqlFormatterPage() {
|
|||
</svg>
|
||||
<span className="font-medium">{status.message}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800">
|
||||
{status.lineCount !== undefined && <span>Lines: <strong>{status.lineCount}</strong></span>}
|
||||
{status.charCount !== undefined && <span>Characters: <strong>{status.charCount}</strong></span>}
|
||||
</div>
|
||||
|
|
@ -709,9 +709,9 @@ export default function SqlFormatterPage() {
|
|||
)}
|
||||
|
||||
{status.type === "error" && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-900 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300">
|
||||
<div className=" border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
|
|
@ -719,8 +719,8 @@ export default function SqlFormatterPage() {
|
|||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-semibold text-red-800 dark:text-red-200">Formatting Error</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-700 dark:text-red-300">{status.message}</p>
|
||||
<p className="font-semibold text-red-400">Formatting Error</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-400">{status.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -732,8 +732,8 @@ export default function SqlFormatterPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Raw SQL Input</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Raw SQL Input</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({inputLineCount} {inputLineCount === 1 ? "line" : "lines"}, {input.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -747,14 +747,14 @@ export default function SqlFormatterPage() {
|
|||
// Permission denied
|
||||
}
|
||||
}}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-[#111] cursor-pointer"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setInput("")}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-red-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-red-400 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-[#111] hover:text-red-400 cursor-pointer"
|
||||
title="Clear input"
|
||||
>
|
||||
Clear
|
||||
|
|
@ -767,7 +767,7 @@ export default function SqlFormatterPage() {
|
|||
placeholder="Enter or paste unformatted SQL queries here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-white p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -775,8 +775,8 @@ export default function SqlFormatterPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Formatted SQL Result</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Formatted SQL Result</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({outputLineCount} {outputLineCount === 1 ? "line" : "lines"}, {output.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -784,7 +784,7 @@ export default function SqlFormatterPage() {
|
|||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={!output && !input}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
className="inline-flex items-center gap-1 bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
title="Copy SQL output"
|
||||
>
|
||||
{copied ? (
|
||||
|
|
@ -816,29 +816,29 @@ export default function SqlFormatterPage() {
|
|||
placeholder="Formatted SQL output will appear here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-gray-50 p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-900/80 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-black p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Highlights & Guide */}
|
||||
<div className="mt-8 rounded-xl border border-gray-200 bg-gray-50/70 p-5 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-800/30 dark:text-gray-400">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">About SQL Formatter</h3>
|
||||
<div className="mt-8 border border-[#1a1a1a] bg-black/70 p-5 text-sm text-gray-600">
|
||||
<h3 className="font-semibold text-gray-200">About SQL Formatter</h3>
|
||||
<div className="mt-3 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">📐 Major Clause Alignment</p>
|
||||
<p className="font-medium text-gray-300">📐 Major Clause Alignment</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Aligns SELECT, FROM, JOIN, WHERE, GROUP BY, HAVING, ORDER BY, and sub-clauses for optimal clarity.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔤 Keyword Casing</p>
|
||||
<p className="font-medium text-gray-300">🔤 Keyword Casing</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Standardize keywords to UPPERCASE or lowercase across ANSI SQL, MySQL, PostgreSQL, SQLite, and SQL Server.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">⚡ Single-Line Minify</p>
|
||||
<p className="font-medium text-gray-300">⚡ Single-Line Minify</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Remove unnecessary linebreaks and comments to compact SQL queries for embedding into code or scripts.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -241,31 +241,31 @@ export default function TimestampConverterPage() {
|
|||
>
|
||||
<div className="space-y-8">
|
||||
{/* Live Current Timestamp Banner */}
|
||||
<div className="rounded-xl border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 dark:border-blue-900/50 dark:from-blue-950/30 dark:to-indigo-950/30 sm:p-6">
|
||||
<div className=" border border-blue-900 bg-gradient-to-r from-blue-50 to-indigo-50 p-4 sm:p-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-3 w-3">
|
||||
<span
|
||||
className={`absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75 ${
|
||||
className={`absolute inline-flex h-full w-full rounded-none bg-emerald-400 opacity-75 ${
|
||||
!isLivePaused ? "animate-ping" : ""
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`relative inline-flex h-3 w-3 rounded-full ${
|
||||
className={`relative inline-flex h-3 w-3 rounded-none ${
|
||||
!isLivePaused ? "bg-emerald-500" : "bg-amber-500"
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-600 dark:text-gray-300">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-600">
|
||||
Current Unix Epoch Time {isLivePaused && "(Paused)"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 font-mono text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl">
|
||||
<div className="mt-2 font-mono text-3xl font-bold tracking-tight text-gray-200 sm:text-4xl">
|
||||
{currentTimestamp}
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-600 dark:text-gray-400 sm:text-sm">
|
||||
<span className="font-medium text-gray-800 dark:text-gray-200">UTC:</span> {nowDate.toUTCString()}
|
||||
<div className="mt-1 text-xs text-gray-600 sm:text-sm">
|
||||
<span className="font-medium text-gray-300">UTC:</span> {nowDate.toUTCString()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -273,7 +273,7 @@ export default function TimestampConverterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(currentTimestamp.toString(), "live-ts")}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 active:scale-95 transition-all"
|
||||
className="flex items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 active:scale-95 transition-all"
|
||||
>
|
||||
{copiedKey === "live-ts" ? (
|
||||
<>
|
||||
|
|
@ -300,7 +300,7 @@ export default function TimestampConverterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setIsLivePaused(!isLivePaused)}
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-400 hover:bg-[#111] transition-colors"
|
||||
>
|
||||
{isLivePaused ? "Resume" : "Pause"}
|
||||
</button>
|
||||
|
|
@ -312,7 +312,7 @@ export default function TimestampConverterPage() {
|
|||
setTsInput(Math.floor(Date.now() / 1000).toString());
|
||||
setDateInput(toDatetimeLocal(new Date()));
|
||||
}}
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 transition-colors"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-400 hover:bg-[#111] transition-colors"
|
||||
title="Reset to current time"
|
||||
>
|
||||
Reset to Now
|
||||
|
|
@ -322,14 +322,14 @@ export default function TimestampConverterPage() {
|
|||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-800">
|
||||
<div className="flex border-b border-[#1a1a1a]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("tsToDate")}
|
||||
className={`flex items-center gap-2 border-b-2 px-4 py-3 text-sm font-semibold transition-colors ${
|
||||
activeTab === "tsToDate"
|
||||
? "border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400"
|
||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "border-blue-600 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:border-[#1a1a1a] hover:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -347,8 +347,8 @@ export default function TimestampConverterPage() {
|
|||
onClick={() => setActiveTab("dateToTs")}
|
||||
className={`flex items-center gap-2 border-b-2 px-4 py-3 text-sm font-semibold transition-colors ${
|
||||
activeTab === "dateToTs"
|
||||
? "border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400"
|
||||
: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
? "border-blue-600 text-blue-600"
|
||||
: "border-transparent text-gray-500 hover:border-[#1a1a1a] hover:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
|
|
@ -366,22 +366,22 @@ export default function TimestampConverterPage() {
|
|||
{/* Tab 1: Timestamp to Date */}
|
||||
{activeTab === "tsToDate" && (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50 sm:p-5">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-4 sm:p-5">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<label htmlFor="ts-input" className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
<label htmlFor="ts-input" className="text-sm font-semibold text-gray-200">
|
||||
Enter Unix Timestamp:
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Unit:</span>
|
||||
<div className="inline-flex rounded-lg border border-gray-300 p-0.5 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<span className="text-xs text-gray-500">Unit:</span>
|
||||
<div className="inline-flex border border-[#1a1a1a] p-0.5 bg-[#0a0a0a]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUnitMode("auto")}
|
||||
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
unitMode === "auto"
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Auto
|
||||
|
|
@ -389,10 +389,10 @@ export default function TimestampConverterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setUnitMode("seconds")}
|
||||
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
unitMode === "seconds"
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Seconds (s)
|
||||
|
|
@ -400,10 +400,10 @@ export default function TimestampConverterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setUnitMode("milliseconds")}
|
||||
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
className={`px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
unitMode === "milliseconds"
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Milliseconds (ms)
|
||||
|
|
@ -419,13 +419,13 @@ export default function TimestampConverterPage() {
|
|||
value={tsInput}
|
||||
onChange={(e) => setTsInput(e.target.value)}
|
||||
placeholder="e.g. 1788114108"
|
||||
className="w-full p-3 font-mono text-base sm:text-lg rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-3 font-mono text-base sm:text-lg border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
{tsInput && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTsInput("")}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 rounded p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-700 dark:hover:text-gray-200"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:bg-[#111] hover:text-gray-600"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
|
|
@ -436,53 +436,53 @@ export default function TimestampConverterPage() {
|
|||
|
||||
{/* Quick Buttons */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 mr-1">Quick presets:</span>
|
||||
<span className="text-xs font-medium text-gray-500 mr-1">Quick presets:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickTs("now")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
Now
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickTs("todayStart")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
Start of Today
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickTs("todayEnd")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
End of Today
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickTs("minusHour")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
-1 Hour
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickTs("plusHour")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
+1 Hour
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickTs("minusDay")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
-1 Day
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickTs("plusDay")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
+1 Day
|
||||
</button>
|
||||
|
|
@ -500,7 +500,7 @@ export default function TimestampConverterPage() {
|
|||
prefix="ts"
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
|
||||
<div className=" border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-400">
|
||||
⚠️ {parsedFromTs.error}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -510,22 +510,22 @@ export default function TimestampConverterPage() {
|
|||
{/* Tab 2: Date to Timestamp */}
|
||||
{activeTab === "dateToTs" && (
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-900/50 sm:p-5">
|
||||
<div className=" border border-[#1a1a1a] bg-black p-4 sm:p-5">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<label htmlFor="datetime-picker" className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
<label htmlFor="datetime-picker" className="text-sm font-semibold text-gray-200">
|
||||
Select Date & Time:
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">Interpret as:</span>
|
||||
<div className="inline-flex rounded-lg border border-gray-300 p-0.5 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||
<span className="text-xs text-gray-500">Interpret as:</span>
|
||||
<div className="inline-flex border border-[#1a1a1a] p-0.5 bg-[#0a0a0a]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDateInputTz("local")}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
className={`px-3 py-1 text-xs font-medium transition-colors ${
|
||||
dateInputTz === "local"
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
Local Timezone
|
||||
|
|
@ -533,10 +533,10 @@ export default function TimestampConverterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setDateInputTz("utc")}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
|
||||
className={`px-3 py-1 text-xs font-medium transition-colors ${
|
||||
dateInputTz === "utc"
|
||||
? "bg-blue-600 text-white"
|
||||
: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
|
||||
: "text-gray-600 hover:text-gray-200"
|
||||
}`}
|
||||
>
|
||||
UTC Time
|
||||
|
|
@ -553,14 +553,14 @@ export default function TimestampConverterPage() {
|
|||
step="1"
|
||||
value={dateInput}
|
||||
onChange={(e) => setDateInput(e.target.value)}
|
||||
className="w-full p-3 font-mono text-base rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||
className="w-full p-3 font-mono text-base border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickDate("now")}
|
||||
className="flex-1 py-3 px-3 rounded-lg bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 text-sm font-medium transition-colors"
|
||||
className="flex-1 py-3 px-3 bg-[#1a1a1a] hover:bg-[#222] text-gray-300 text-sm font-medium transition-colors"
|
||||
>
|
||||
Set to Now
|
||||
</button>
|
||||
|
|
@ -569,32 +569,32 @@ export default function TimestampConverterPage() {
|
|||
|
||||
{/* Quick presets for date */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
||||
<span className="text-xs font-medium text-gray-500 dark:text-gray-400 mr-1">Quick presets:</span>
|
||||
<span className="text-xs font-medium text-gray-500 mr-1">Quick presets:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickDate("todayStart")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
Start of Today
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickDate("todayEnd")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
End of Today
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickDate("minusDay")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
Yesterday
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleQuickDate("plusDay")}
|
||||
className="rounded bg-white px-2.5 py-1 text-xs font-medium text-gray-700 border border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
className="rounded bg-[#0a0a0a] px-2.5 py-1 text-xs font-medium text-gray-400 border border-[#1a1a1a] hover:bg-[#111]"
|
||||
>
|
||||
Tomorrow
|
||||
</button>
|
||||
|
|
@ -612,7 +612,7 @@ export default function TimestampConverterPage() {
|
|||
prefix="date"
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/30 dark:text-red-300">
|
||||
<div className=" border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-400">
|
||||
⚠️ {parsedFromDate.error}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -620,8 +620,8 @@ export default function TimestampConverterPage() {
|
|||
)}
|
||||
|
||||
{/* Cheat sheet / Developer Reference */}
|
||||
<div className="mt-8 rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-800 dark:bg-gray-900/50">
|
||||
<h3 className="text-base font-bold text-gray-900 dark:text-white mb-3">
|
||||
<div className="mt-8 border border-[#1a1a1a] bg-black p-5">
|
||||
<h3 className="text-base font-bold text-gray-200 mb-3">
|
||||
Developer Reference: Getting Current Timestamp
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
|
@ -638,31 +638,31 @@ export default function TimestampConverterPage() {
|
|||
].map((item) => (
|
||||
<div
|
||||
key={item.lang}
|
||||
className="rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] p-3"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-semibold text-gray-500 dark:text-gray-400">{item.lang}</span>
|
||||
<span className="text-xs font-semibold text-gray-500">{item.lang}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(item.code, `code-${item.lang}`)}
|
||||
className="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
className="text-xs text-blue-600 hover:text-blue-400"
|
||||
>
|
||||
{copiedKey === `code-${item.lang}` ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<code className="block font-mono text-xs text-gray-800 dark:text-gray-200 overflow-x-auto whitespace-nowrap">
|
||||
<code className="block font-mono text-xs text-gray-300 overflow-x-auto whitespace-nowrap">
|
||||
{item.code}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex flex-wrap gap-x-6 gap-y-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<div><strong className="text-gray-800 dark:text-gray-200">1 hour:</strong> 3,600 seconds</div>
|
||||
<div><strong className="text-gray-800 dark:text-gray-200">1 day:</strong> 86,400 seconds</div>
|
||||
<div><strong className="text-gray-800 dark:text-gray-200">1 week:</strong> 604,800 seconds</div>
|
||||
<div><strong className="text-gray-800 dark:text-gray-200">30 days:</strong> 2,592,000 seconds</div>
|
||||
<div><strong className="text-gray-800 dark:text-gray-200">365 days:</strong> 31,536,000 seconds</div>
|
||||
<div className="mt-4 pt-4 border-t border-[#1a1a1a] flex flex-wrap gap-x-6 gap-y-2 text-xs text-gray-600">
|
||||
<div><strong className="text-gray-300">1 hour:</strong> 3,600 seconds</div>
|
||||
<div><strong className="text-gray-300">1 day:</strong> 86,400 seconds</div>
|
||||
<div><strong className="text-gray-300">1 week:</strong> 604,800 seconds</div>
|
||||
<div><strong className="text-gray-300">30 days:</strong> 2,592,000 seconds</div>
|
||||
<div><strong className="text-gray-300">365 days:</strong> 31,536,000 seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -727,7 +727,7 @@ function FormatResultsGrid({ date, nowDate, copiedKey, onCopy, prefix }: FormatR
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-400">
|
||||
Converted Formats
|
||||
</h3>
|
||||
|
||||
|
|
@ -739,23 +739,23 @@ function FormatResultsGrid({ date, nowDate, copiedKey, onCopy, prefix }: FormatR
|
|||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`group flex flex-col justify-between rounded-xl border p-4 transition-all ${
|
||||
className={`group flex flex-col justify-between border p-4 transition-all ${
|
||||
item.highlight
|
||||
? "border-blue-200 bg-blue-50/50 dark:border-blue-900/40 dark:bg-blue-950/20"
|
||||
: "border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-800/80"
|
||||
? "border-blue-900 bg-[#0a0a1a]/50"
|
||||
: "border-[#1a1a1a] bg-[#0a0a0a]"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500">
|
||||
{item.label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCopy(item.value, key)}
|
||||
className={`flex items-center gap-1 text-xs font-medium px-2 py-1 rounded transition-colors ${
|
||||
className={`flex items-center gap-1 text-xs font-medium px-2 py-1 transition-colors ${
|
||||
isCopied
|
||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300"
|
||||
: "bg-gray-100 hover:bg-gray-200 text-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600 dark:text-gray-300"
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-[#111] hover:bg-[#1a1a1a] text-gray-400"
|
||||
}`}
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
|
|
@ -782,7 +782,7 @@ function FormatResultsGrid({ date, nowDate, copiedKey, onCopy, prefix }: FormatR
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className="font-mono text-sm font-medium text-gray-900 dark:text-white break-all">
|
||||
<div className="font-mono text-sm font-medium text-gray-200 break-all">
|
||||
{item.value}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -792,14 +792,14 @@ function FormatResultsGrid({ date, nowDate, copiedKey, onCopy, prefix }: FormatR
|
|||
|
||||
{/* Date metadata badges */}
|
||||
<div className="flex flex-wrap gap-2 pt-2 text-xs">
|
||||
<span className="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 font-medium text-gray-800 dark:bg-gray-800 dark:text-gray-300">
|
||||
Day of Year: <strong className="ml-1 text-gray-900 dark:text-white">{dayOfYear}</strong>
|
||||
<span className="inline-flex items-center bg-[#111] px-2.5 py-1 font-medium text-gray-300">
|
||||
Day of Year: <strong className="ml-1 text-gray-200">{dayOfYear}</strong>
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 font-medium text-gray-800 dark:bg-gray-800 dark:text-gray-300">
|
||||
ISO Week: <strong className="ml-1 text-gray-900 dark:text-white">#{weekNum}</strong>
|
||||
<span className="inline-flex items-center bg-[#111] px-2.5 py-1 font-medium text-gray-300">
|
||||
ISO Week: <strong className="ml-1 text-gray-200">#{weekNum}</strong>
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 font-medium text-gray-800 dark:bg-gray-800 dark:text-gray-300">
|
||||
Leap Year: <strong className="ml-1 text-gray-900 dark:text-white">{leap ? "Yes" : "No"}</strong>
|
||||
<span className="inline-flex items-center bg-[#111] px-2.5 py-1 font-medium text-gray-300">
|
||||
Leap Year: <strong className="ml-1 text-gray-200">{leap ? "Yes" : "No"}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -144,12 +144,12 @@ export default function UrlEncoderPage() {
|
|||
|
||||
<div className="space-y-6">
|
||||
{/* Controls Bar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-2 border-b border-[#1a1a1a]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleEncode}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
|
|
@ -159,7 +159,7 @@ export default function UrlEncoderPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleDecode}
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white rounded-lg font-medium text-sm transition-colors shadow-sm cursor-pointer flex items-center gap-1.5"
|
||||
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white font-medium text-sm transition-colors cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
|
|
@ -170,7 +170,7 @@ export default function UrlEncoderPage() {
|
|||
type="button"
|
||||
onClick={handleSwap}
|
||||
disabled={!output && !input}
|
||||
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-200 rounded-lg text-sm font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-2 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-sm font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
title="Swap input and output"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -182,7 +182,7 @@ export default function UrlEncoderPage() {
|
|||
type="button"
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="px-3 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-red-50 dark:hover:bg-red-950/30 text-gray-700 dark:text-gray-300 hover:text-red-600 dark:hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed rounded-lg text-sm font-medium transition-colors cursor-pointer"
|
||||
className="px-3 py-2 bg-[#111] hover:bg-[#1a0a0a] text-gray-400 hover:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed text-sm font-medium transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -190,25 +190,25 @@ export default function UrlEncoderPage() {
|
|||
|
||||
{/* Sample buttons */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-gray-500 dark:text-gray-400 font-medium">Samples:</span>
|
||||
<span className="text-gray-500 font-medium">Samples:</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("param")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Query Param
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("fullUrl")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Full URL
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadSample("encoded")}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 rounded transition-colors cursor-pointer"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] text-gray-400 transition-colors cursor-pointer"
|
||||
>
|
||||
Encoded URI
|
||||
</button>
|
||||
|
|
@ -216,9 +216,9 @@ export default function UrlEncoderPage() {
|
|||
</div>
|
||||
|
||||
{/* Encoding Options */}
|
||||
<div className="flex flex-wrap items-center gap-6 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800/50 p-3 rounded-lg border border-gray-200 dark:border-gray-700/60">
|
||||
<div className="flex flex-wrap items-center gap-6 text-sm text-gray-400 bg-black p-3 border border-[#1a1a1a]">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Encode Mode:</span>
|
||||
<span className="font-medium text-xs text-gray-500 uppercase tracking-wider">Encode Mode:</span>
|
||||
<label className="inline-flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
|
|
@ -231,7 +231,7 @@ export default function UrlEncoderPage() {
|
|||
encodeUrl(input, "component", spaceAsPlus);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>encodeURIComponent <span className="text-xs text-gray-500">(for query params & values)</span></span>
|
||||
</label>
|
||||
|
|
@ -247,7 +247,7 @@ export default function UrlEncoderPage() {
|
|||
encodeUrl(input, "fullUrl", spaceAsPlus);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>encodeURI <span className="text-xs text-gray-500">(preserves full URL structure)</span></span>
|
||||
</label>
|
||||
|
|
@ -265,9 +265,9 @@ export default function UrlEncoderPage() {
|
|||
encodeUrl(input, encodeMode, val);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Encode spaces as <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">+</code></span>
|
||||
<span>Encode spaces as <code className="text-xs bg-[#1a1a1a] px-1 py-0.5 font-mono">+</code></span>
|
||||
</label>
|
||||
|
||||
<label className="inline-flex items-center gap-2 cursor-pointer select-none">
|
||||
|
|
@ -281,16 +281,16 @@ export default function UrlEncoderPage() {
|
|||
decodeUrl(input, val);
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 focus:ring-blue-500"
|
||||
className="w-4 h-4 text-blue-600 border-[#1a1a1a] focus:ring-blue-900"
|
||||
/>
|
||||
<span>Decode <code className="text-xs bg-gray-200 dark:bg-gray-700 px-1 py-0.5 rounded font-mono">+</code> as spaces</span>
|
||||
<span>Decode <code className="text-xs bg-[#1a1a1a] px-1 py-0.5 font-mono">+</code> as spaces</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Notification */}
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-800 rounded-lg text-red-800 dark:text-red-300 text-sm">
|
||||
<div className="flex items-start gap-3 p-4 bg-[#1a0a0a] border border-red-900 text-red-400 text-sm">
|
||||
<svg className="w-5 h-5 text-red-500 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
|
|
@ -306,10 +306,10 @@ export default function UrlEncoderPage() {
|
|||
{/* Input Area */}
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={inputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={inputId} className="font-medium text-sm text-gray-400">
|
||||
Input Text or URL
|
||||
</label>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{input.length} chars
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -322,7 +322,7 @@ export default function UrlEncoderPage() {
|
|||
}}
|
||||
placeholder="Enter text or URL to encode or decode..."
|
||||
rows={12}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed shadow-inner"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-mono text-sm focus:ring-1 focus:ring-blue-900 focus:outline-none resize-y placeholder:text-gray-400 leading-relaxed shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -330,31 +330,31 @@ export default function UrlEncoderPage() {
|
|||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor={outputId} className="font-medium text-sm text-gray-700 dark:text-gray-300">
|
||||
<label htmlFor={outputId} className="font-medium text-sm text-gray-400">
|
||||
Output Result
|
||||
</label>
|
||||
{lastAction && (
|
||||
<span className="px-2 py-0.5 text-xs rounded-full font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300">
|
||||
<span className="px-2 py-0.5 text-xs rounded-none font-medium bg-[#0a0a1a] text-blue-400">
|
||||
{lastAction === "encode" ? "URL Encoded" : "URL Decoded"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-mono">
|
||||
<span className="text-xs text-gray-500 font-mono">
|
||||
{output.length} chars
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
disabled={!output}
|
||||
className="px-2.5 py-1 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 rounded text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-2.5 py-1 bg-[#111] hover:bg-[#1a1a1a] disabled:opacity-40 disabled:cursor-not-allowed text-gray-400 text-xs font-medium transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-green-600 dark:text-green-400 font-semibold">Copied!</span>
|
||||
<span className="text-green-400 font-semibold">Copied!</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -373,38 +373,38 @@ export default function UrlEncoderPage() {
|
|||
readOnly
|
||||
placeholder="Encoded or decoded output will appear here..."
|
||||
rows={12}
|
||||
className="w-full p-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/60 text-gray-900 dark:text-gray-100 font-mono text-sm focus:outline-none resize-y placeholder:text-gray-400 dark:placeholder:text-gray-500 leading-relaxed select-all"
|
||||
className="w-full p-3 border border-[#1a1a1a] bg-black text-gray-200 font-mono text-sm focus:outline-none resize-y placeholder:text-gray-400 leading-relaxed select-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Reference Table */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200 dark:border-gray-800">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">Common URL Percent-Encoding Reference</h3>
|
||||
<div className="mt-8 pt-6 border-t border-[#1a1a1a]">
|
||||
<h3 className="text-sm font-semibold text-gray-200 mb-3">Common URL Percent-Encoding Reference</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-6 gap-2 text-xs font-mono">
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Space:</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">%20 or +</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-600">Space:</span>
|
||||
<span className="font-semibold text-blue-600">%20 or +</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">/ (slash):</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">%2F</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-600">/ (slash):</span>
|
||||
<span className="font-semibold text-blue-600">%2F</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">: (colon):</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">%3A</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-600">: (colon):</span>
|
||||
<span className="font-semibold text-blue-600">%3A</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">& (ampersand):</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">%26</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-600">& (ampersand):</span>
|
||||
<span className="font-semibold text-blue-600">%26</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">? (question):</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">%3F</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-600">? (question):</span>
|
||||
<span className="font-semibold text-blue-600">%3F</span>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-50 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">= (equal):</span>
|
||||
<span className="font-semibold text-blue-600 dark:text-blue-400">%3D</span>
|
||||
<div className="p-2 bg-black border border-[#1a1a1a] flex justify-between">
|
||||
<span className="text-gray-600">= (equal):</span>
|
||||
<span className="font-semibold text-blue-600">%3D</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -131,11 +131,11 @@ export default function UuidGeneratorPage() {
|
|||
>
|
||||
<div className="space-y-6">
|
||||
{/* Controls Bar */}
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/50 p-4 dark:border-gray-700/60 dark:bg-gray-800/40">
|
||||
<div className=" border border-[#1a1a1a] bg-black/50 p-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Quantity Selector */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Quantity (1 - 100)
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -145,7 +145,7 @@ export default function UuidGeneratorPage() {
|
|||
max="100"
|
||||
value={count}
|
||||
onChange={(e) => handleCountChange(parseInt(e.target.value) || 1)}
|
||||
className="w-24 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
||||
className="w-24 border border-[#1a1a1a] bg-[#0a0a0a] px-3 py-2 text-sm font-medium text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900/20"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{countPresets.map((preset) => (
|
||||
|
|
@ -156,7 +156,7 @@ export default function UuidGeneratorPage() {
|
|||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
count === preset
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||
: "bg-[#1a1a1a] text-gray-400 hover:bg-[#222]"
|
||||
}`}
|
||||
>
|
||||
{preset}
|
||||
|
|
@ -168,25 +168,25 @@ export default function UuidGeneratorPage() {
|
|||
|
||||
{/* Options Toggles */}
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-700 dark:text-gray-300">
|
||||
<label className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-gray-400">
|
||||
Formatting Options
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-4 pt-1">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={uppercase}
|
||||
onChange={handleUppercaseToggle}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Uppercase (A-F)</span>
|
||||
</label>
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2 text-sm text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={hyphens}
|
||||
onChange={handleHyphensToggle}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Include Hyphens</span>
|
||||
</label>
|
||||
|
|
@ -198,7 +198,7 @@ export default function UuidGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleGenerate}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-5 py-2.5 font-medium text-white shadow-sm transition-all hover:bg-blue-700 active:scale-[0.99] focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
|
||||
className="flex w-full items-center justify-center gap-2 bg-blue-600 px-5 py-2.5 font-medium text-white transition-all hover:bg-blue-700 active:scale-[0.99] focus:outline-none focus:ring-1 focus:ring-blue-900 focus:ring-offset-2"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
|
|
@ -220,12 +220,12 @@ export default function UuidGeneratorPage() {
|
|||
</div>
|
||||
|
||||
{/* Results Header / Bulk Actions */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-3 dark:border-gray-700">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<span className="text-sm font-medium text-gray-400">
|
||||
Generated UUIDs
|
||||
</span>
|
||||
<span className="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-semibold text-blue-800 dark:bg-blue-900/40 dark:text-blue-300">
|
||||
<span className="inline-flex items-center rounded-none bg-[#0a0a1a] px-2.5 py-0.5 text-xs font-semibold text-blue-400">
|
||||
{uuids.length}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -234,10 +234,10 @@ export default function UuidGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={copyAllToClipboard}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
copiedAll
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
: "bg-[#111] text-gray-400 hover:bg-[#1a1a1a]"
|
||||
}`}
|
||||
>
|
||||
{copiedAll ? (
|
||||
|
|
@ -266,7 +266,7 @@ export default function UuidGeneratorPage() {
|
|||
type="button"
|
||||
onClick={downloadAsText}
|
||||
title="Download as .txt"
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
className="inline-flex items-center gap-1.5 bg-[#111] px-3 py-1.5 text-xs font-medium text-gray-400 transition-colors hover:bg-[#1a1a1a]"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -283,7 +283,7 @@ export default function UuidGeneratorPage() {
|
|||
type="button"
|
||||
onClick={downloadAsJson}
|
||||
title="Download as .json"
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-1.5 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
|
||||
className="inline-flex items-center gap-1.5 bg-[#111] px-3 py-1.5 text-xs font-medium text-gray-400 transition-colors hover:bg-[#1a1a1a]"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -300,19 +300,19 @@ export default function UuidGeneratorPage() {
|
|||
|
||||
{/* Featured Single UUID Card when count is 1 */}
|
||||
{uuids.length === 1 && (
|
||||
<div className="flex flex-col items-center justify-between gap-4 rounded-xl border border-blue-200 bg-blue-50/50 p-6 dark:border-blue-900/50 dark:bg-blue-950/20 sm:flex-row">
|
||||
<div className="flex flex-col items-center justify-between gap-4 border border-blue-900 bg-[#0a0a1a]/50 p-6 sm:flex-row">
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-blue-700 dark:text-blue-400">
|
||||
<span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-blue-400">
|
||||
Generated UUID v4
|
||||
</span>
|
||||
<p className="select-all break-all font-mono text-lg font-bold text-gray-900 dark:text-white sm:text-2xl">
|
||||
<p className="select-all break-all font-mono text-lg font-bold text-gray-200 sm:text-2xl">
|
||||
{uuids[0]}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(uuids[0], 0)}
|
||||
className={`flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-all ${
|
||||
className={`flex items-center gap-2 px-5 py-2.5 text-sm font-medium transition-all ${
|
||||
copiedIndex === 0
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-blue-600 text-white hover:bg-blue-700 active:scale-95"
|
||||
|
|
@ -344,18 +344,18 @@ export default function UuidGeneratorPage() {
|
|||
|
||||
{/* UUID List for multiple */}
|
||||
{uuids.length > 1 && (
|
||||
<div className="max-h-[500px] overflow-y-auto rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900/60">
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
<div className="max-h-[500px] overflow-y-auto border border-[#1a1a1a] bg-black">
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
{uuids.map((uuid, idx) => (
|
||||
<div
|
||||
key={`${uuid}-${idx}`}
|
||||
className="group flex items-center justify-between gap-4 p-3 transition-colors hover:bg-white dark:hover:bg-gray-800/80"
|
||||
className="group flex items-center justify-between gap-4 p-3 transition-colors hover:bg-[#0a0a0a]"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="w-8 select-none text-right font-mono text-xs text-gray-400 dark:text-gray-500">
|
||||
<span className="w-8 select-none text-right font-mono text-xs text-gray-400">
|
||||
{idx + 1}.
|
||||
</span>
|
||||
<span className="select-all truncate font-mono text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
<span className="select-all truncate font-mono text-sm font-medium text-gray-200">
|
||||
{uuid}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -363,10 +363,10 @@ export default function UuidGeneratorPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => copyToClipboard(uuid, idx)}
|
||||
className={`flex shrink-0 items-center gap-1 rounded-md px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
className={`flex shrink-0 items-center gap-1 px-2.5 py-1 text-xs font-medium transition-colors ${
|
||||
copiedIndex === idx
|
||||
? "bg-green-600 text-white"
|
||||
: "bg-white text-gray-700 opacity-90 shadow-sm hover:bg-gray-100 group-hover:opacity-100 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
: "bg-[#0a0a0a] text-gray-400 opacity-90 hover:bg-[#111] group-hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
{copiedIndex === idx ? (
|
||||
|
|
@ -402,21 +402,21 @@ export default function UuidGeneratorPage() {
|
|||
)}
|
||||
|
||||
{/* Informational Details */}
|
||||
<div className="grid grid-cols-1 gap-4 pt-4 text-xs text-gray-500 dark:text-gray-400 sm:grid-cols-3">
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">128-bit Randomness</h4>
|
||||
<div className="grid grid-cols-1 gap-4 pt-4 text-xs text-gray-500 sm:grid-cols-3">
|
||||
<div className=" border border-gray-100 bg-black p-3">
|
||||
<h4 className="font-semibold text-gray-300">128-bit Randomness</h4>
|
||||
<p className="mt-1">
|
||||
UUID v4 utilizes 122 cryptographically secure random bits, making collisions statistically virtually impossible.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">100% Client-Side</h4>
|
||||
<div className=" border border-gray-100 bg-black p-3">
|
||||
<h4 className="font-semibold text-gray-300">100% Client-Side</h4>
|
||||
<p className="mt-1">
|
||||
All identifiers are created locally in your browser using the Web Crypto API. No server logs or tracking.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-100 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800/40">
|
||||
<h4 className="font-semibold text-gray-800 dark:text-gray-200">RFC 4122 Compliant</h4>
|
||||
<div className=" border border-gray-100 bg-black p-3">
|
||||
<h4 className="font-semibold text-gray-300">RFC 4122 Compliant</h4>
|
||||
<p className="mt-1">
|
||||
Generated UUIDs adhere strictly to RFC 4122 specifications with standard version 4 and variant bits.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -159,56 +159,56 @@ export default function WordCounterPage() {
|
|||
<div className="space-y-6">
|
||||
{/* Primary Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
||||
<div className="rounded-xl border border-blue-100 bg-blue-50/60 p-4 text-center dark:border-blue-900/40 dark:bg-blue-950/30">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-blue-600 dark:text-blue-400">
|
||||
<div className=" border border-blue-100 bg-[#0a0a1a]/60 p-4 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-blue-600">
|
||||
Words
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-blue-950 dark:text-blue-100">
|
||||
<p className="mt-1 text-2xl font-extrabold text-blue-950">
|
||||
{stats.wordCount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-indigo-100 bg-indigo-50/60 p-4 text-center dark:border-indigo-900/40 dark:bg-indigo-950/30">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-indigo-600 dark:text-indigo-400">
|
||||
<div className=" border border-indigo-100 bg-indigo-50/60 p-4 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-indigo-600">
|
||||
Characters
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-indigo-950 dark:text-indigo-100">
|
||||
<p className="mt-1 text-2xl font-extrabold text-indigo-950">
|
||||
{stats.charCountWithSpaces.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-purple-100 bg-purple-50/60 p-4 text-center dark:border-purple-900/40 dark:bg-purple-950/30">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-purple-600 dark:text-purple-400">
|
||||
<div className=" border border-purple-100 bg-purple-50/60 p-4 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-purple-600">
|
||||
No Spaces
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-purple-950 dark:text-purple-100">
|
||||
<p className="mt-1 text-2xl font-extrabold text-purple-950">
|
||||
{stats.charCountWithoutSpaces.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-teal-100 bg-teal-50/60 p-4 text-center dark:border-teal-900/40 dark:bg-teal-950/30">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-teal-600 dark:text-teal-400">
|
||||
<div className=" border border-teal-100 bg-teal-50/60 p-4 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-teal-600">
|
||||
Sentences
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-teal-950 dark:text-teal-100">
|
||||
<p className="mt-1 text-2xl font-extrabold text-teal-950">
|
||||
{stats.sentenceCount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-amber-100 bg-amber-50/60 p-4 text-center dark:border-amber-900/40 dark:bg-amber-950/30">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-amber-600 dark:text-amber-400">
|
||||
<div className=" border border-amber-100 bg-amber-50/60 p-4 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-amber-600">
|
||||
Paragraphs
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-amber-950 dark:text-amber-100">
|
||||
<p className="mt-1 text-2xl font-extrabold text-amber-950">
|
||||
{stats.paragraphCount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-emerald-100 bg-emerald-50/60 p-4 text-center dark:border-emerald-900/40 dark:bg-emerald-950/30">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-emerald-600 dark:text-emerald-400">
|
||||
<div className=" border border-emerald-100 bg-emerald-50/60 p-4 text-center">
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-emerald-600">
|
||||
Reading Time
|
||||
</span>
|
||||
<p className="mt-1 text-2xl font-extrabold text-emerald-950 dark:text-emerald-100">
|
||||
<p className="mt-1 text-2xl font-extrabold text-emerald-950">
|
||||
{stats.readingTimeStr}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -219,7 +219,7 @@ export default function WordCounterPage() {
|
|||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<label
|
||||
htmlFor="word-counter-input"
|
||||
className="text-sm font-semibold text-gray-700 dark:text-gray-300"
|
||||
className="text-sm font-semibold text-gray-400"
|
||||
>
|
||||
Enter or Paste Your Text
|
||||
</label>
|
||||
|
|
@ -227,7 +227,7 @@ export default function WordCounterPage() {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleLoadSample}
|
||||
className="px-3 py-1.5 text-xs font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-950/50 rounded-md transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs font-medium text-blue-600 hover:bg-[#0a0a1a] transition-colors cursor-pointer"
|
||||
>
|
||||
Load Sample
|
||||
</button>
|
||||
|
|
@ -235,7 +235,7 @@ export default function WordCounterPage() {
|
|||
type="button"
|
||||
onClick={handleCleanWhitespace}
|
||||
disabled={!text}
|
||||
className="px-3 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800 rounded-md disabled:opacity-40 transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs font-medium text-gray-600 hover:bg-[#111] disabled:opacity-40 transition-colors cursor-pointer"
|
||||
>
|
||||
Clean Spaces
|
||||
</button>
|
||||
|
|
@ -243,7 +243,7 @@ export default function WordCounterPage() {
|
|||
type="button"
|
||||
onClick={handleClear}
|
||||
disabled={!text}
|
||||
className="px-3 py-1.5 text-xs font-medium text-rose-600 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950/50 rounded-md disabled:opacity-40 transition-colors cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs font-medium text-rose-600 hover:bg-rose-50 disabled:opacity-40 transition-colors cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
|
|
@ -251,7 +251,7 @@ export default function WordCounterPage() {
|
|||
type="button"
|
||||
onClick={handleCopy}
|
||||
disabled={!text}
|
||||
className="px-3 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-700 text-white rounded-md disabled:opacity-40 transition-colors cursor-pointer flex items-center gap-1"
|
||||
className="px-3 py-1.5 text-xs font-medium bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-40 transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
{copied ? "Copied!" : "Copy Text"}
|
||||
</button>
|
||||
|
|
@ -264,46 +264,46 @@ export default function WordCounterPage() {
|
|||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="Type or paste your text here to get instant word count, character statistics, sentence counts, and reading time..."
|
||||
className="w-full p-3.5 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-sans text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 focus:outline-none transition-shadow resize-y shadow-inner leading-relaxed"
|
||||
className="w-full p-3.5 border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 font-sans text-sm focus:ring-1 focus:ring-blue-900 focus:border-blue-500 focus:outline-none transition-shadow resize-y shadow-inner leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Secondary Detailed Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/70 p-3.5 dark:border-gray-800 dark:bg-gray-800/50">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-3.5">
|
||||
<span className="text-xs text-gray-500">
|
||||
Average Word Length
|
||||
</span>
|
||||
<p className="mt-1 text-lg font-bold text-gray-800 dark:text-gray-200">
|
||||
<p className="mt-1 text-lg font-bold text-gray-300">
|
||||
{stats.avgWordLength}{" "}
|
||||
<span className="text-xs font-normal text-gray-500">chars</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/70 p-3.5 dark:border-gray-800 dark:bg-gray-800/50">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-3.5">
|
||||
<span className="text-xs text-gray-500">
|
||||
Speaking Time (~130 wpm)
|
||||
</span>
|
||||
<p className="mt-1 text-lg font-bold text-gray-800 dark:text-gray-200">
|
||||
<p className="mt-1 text-lg font-bold text-gray-300">
|
||||
{stats.speakingTimeStr}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/70 p-3.5 dark:border-gray-800 dark:bg-gray-800/50">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-3.5">
|
||||
<span className="text-xs text-gray-500">
|
||||
Total Lines
|
||||
</span>
|
||||
<p className="mt-1 text-lg font-bold text-gray-800 dark:text-gray-200">
|
||||
<p className="mt-1 text-lg font-bold text-gray-300">
|
||||
{stats.lineCount.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-gray-200 bg-gray-50/70 p-3.5 dark:border-gray-800 dark:bg-gray-800/50">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className=" border border-[#1a1a1a] bg-black/70 p-3.5">
|
||||
<span className="text-xs text-gray-500">
|
||||
Longest Word
|
||||
</span>
|
||||
<p
|
||||
className="mt-1 text-lg font-bold text-gray-800 dark:text-gray-200 truncate"
|
||||
className="mt-1 text-lg font-bold text-gray-300 truncate"
|
||||
title={stats.longestWord || "None"}
|
||||
>
|
||||
{stats.longestWord || "—"}{" "}
|
||||
|
|
@ -318,27 +318,27 @@ export default function WordCounterPage() {
|
|||
|
||||
{/* Top Keywords / Frequency Section */}
|
||||
{stats.topKeywords.length > 0 && (
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/40 p-4 dark:border-gray-800 dark:bg-gray-900/40">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
<div className=" border border-[#1a1a1a] bg-black/40 p-4">
|
||||
<h3 className="text-sm font-semibold text-gray-200">
|
||||
Top Keywords & Frequency
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
Most frequently occurring terms in your text.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2.5">
|
||||
{stats.topKeywords.map((kw, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between p-2.5 rounded-lg border border-gray-200/80 bg-white dark:border-gray-800 dark:bg-gray-800 text-xs shadow-xs"
|
||||
className="flex items-center justify-between p-2.5 border border-[#1a1a1a]/80 bg-[#0a0a0a] text-xs shadow-xs"
|
||||
>
|
||||
<span className="font-mono font-medium text-gray-800 dark:text-gray-200 truncate mr-2">
|
||||
<span className="font-mono font-medium text-gray-300 truncate mr-2">
|
||||
{kw.word}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span className="px-2 py-0.5 rounded-full bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300 font-semibold">
|
||||
<span className="px-2 py-0.5 rounded-none bg-[#0a0a1a] text-blue-400 font-semibold">
|
||||
{kw.count}×
|
||||
</span>
|
||||
<span className="text-gray-400 dark:text-gray-500">
|
||||
<span className="text-gray-400">
|
||||
{kw.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -396,11 +396,11 @@ export default function XmlFormatterPage() {
|
|||
>
|
||||
<div className="space-y-5">
|
||||
{/* Top Controls Toolbar */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-gray-200 pb-4 dark:border-gray-800">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#1a1a1a] pb-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
onClick={handleFormat}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 active:scale-95 cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h7" />
|
||||
|
|
@ -410,7 +410,7 @@ export default function XmlFormatterPage() {
|
|||
|
||||
<button
|
||||
onClick={handleMinify}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-800 transition-colors hover:bg-gray-300 active:scale-95 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#1a1a1a] px-4 py-2 text-sm font-medium text-gray-300 transition-colors hover:bg-[#222] active:scale-95 cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
|
|
@ -420,7 +420,7 @@ export default function XmlFormatterPage() {
|
|||
|
||||
<button
|
||||
onClick={handleValidate}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700 active:scale-95 dark:bg-emerald-700 dark:hover:bg-emerald-600 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-emerald-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700 active:scale-95 cursor-pointer"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
|
|
@ -430,7 +430,7 @@ export default function XmlFormatterPage() {
|
|||
|
||||
<button
|
||||
onClick={handleLoadSample}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-blue-50 px-3 py-2 text-sm font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:bg-blue-950/40 dark:text-blue-300 dark:hover:bg-blue-900/50 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#0a0a1a] px-3 py-2 text-sm font-medium text-blue-400 transition-colors hover:bg-[#0a0a1a] cursor-pointer"
|
||||
>
|
||||
Load Sample
|
||||
</button>
|
||||
|
|
@ -438,24 +438,24 @@ export default function XmlFormatterPage() {
|
|||
<button
|
||||
onClick={handleClear}
|
||||
disabled={!input && !output}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg bg-gray-100 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-rose-950/30 dark:hover:text-rose-400 cursor-pointer"
|
||||
className="inline-flex items-center gap-1.5 bg-[#111] px-3 py-2 text-sm font-medium text-gray-400 transition-colors hover:bg-rose-50 hover:text-rose-600 disabled:opacity-40 cursor-pointer"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-400">
|
||||
{/* Indent Selector */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<label htmlFor={indentSelectId} className="text-xs font-semibold text-gray-500 uppercase dark:text-gray-400">
|
||||
<label htmlFor={indentSelectId} className="text-xs font-semibold text-gray-500 uppercase">
|
||||
Indent:
|
||||
</label>
|
||||
<select
|
||||
id={indentSelectId}
|
||||
value={indentStr}
|
||||
onChange={(e) => setIndentStr(e.target.value)}
|
||||
className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-800 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
className=" border border-[#1a1a1a] bg-[#0a0a0a] px-2 py-1 text-xs text-gray-300 shadow-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-900"
|
||||
>
|
||||
<option value=" ">2 Spaces</option>
|
||||
<option value=" ">4 Spaces</option>
|
||||
|
|
@ -464,12 +464,12 @@ export default function XmlFormatterPage() {
|
|||
</div>
|
||||
|
||||
{/* Collapse Empty Tags */}
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-xs text-gray-600 select-none hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100">
|
||||
<label className="flex cursor-pointer items-center gap-1.5 text-xs text-gray-600 select-none hover:text-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={collapseEmpty}
|
||||
onChange={(e) => setCollapseEmpty(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
|
||||
className="h-4 w-4 border-[#1a1a1a] text-blue-600 focus:ring-blue-900"
|
||||
/>
|
||||
<span>Self-close empty tags (<code><tag /></code>)</span>
|
||||
</label>
|
||||
|
|
@ -478,9 +478,9 @@ export default function XmlFormatterPage() {
|
|||
|
||||
{/* Status Notification Banner */}
|
||||
{status.type === "success" && (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border border-emerald-300 bg-emerald-50 px-4 py-3 text-sm text-emerald-900">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600 dark:text-emerald-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="h-5 w-5 shrink-0 text-emerald-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
|
|
@ -489,7 +489,7 @@ export default function XmlFormatterPage() {
|
|||
</svg>
|
||||
<span className="font-medium">{status.message}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800 dark:text-emerald-300">
|
||||
<div className="flex flex-wrap items-center gap-3 text-xs text-emerald-800">
|
||||
{status.lineCount !== undefined && <span>Lines: <strong>{status.lineCount}</strong></span>}
|
||||
{status.byteSize !== undefined && <span>Size: <strong>{formatBytes(status.byteSize)}</strong></span>}
|
||||
</div>
|
||||
|
|
@ -497,9 +497,9 @@ export default function XmlFormatterPage() {
|
|||
)}
|
||||
|
||||
{status.type === "error" && (
|
||||
<div className="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-900 dark:border-red-800 dark:bg-red-950/40 dark:text-red-300">
|
||||
<div className=" border border-red-900 bg-[#1a0a0a] p-4 text-sm text-red-900">
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-600 dark:text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="mt-0.5 h-5 w-5 shrink-0 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
||||
|
|
@ -507,10 +507,10 @@ export default function XmlFormatterPage() {
|
|||
/>
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-red-800 dark:text-red-200">XML Parsing Error</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-700 dark:text-red-300">{status.message}</p>
|
||||
<p className="font-semibold text-red-400">XML Parsing Error</p>
|
||||
<p className="mt-1 font-mono text-xs text-red-400">{status.message}</p>
|
||||
{(status.line !== undefined || status.column !== undefined) && (
|
||||
<div className="mt-2 inline-flex items-center gap-2 rounded bg-red-100 px-2.5 py-1 text-xs font-medium text-red-800 dark:bg-red-900/60 dark:text-red-200">
|
||||
<div className="mt-2 inline-flex items-center gap-2 bg-[#1a0a0a] px-2.5 py-1 text-xs font-medium text-red-400">
|
||||
<span>📍 Error location:</span>
|
||||
{status.line !== undefined && <span>Line <strong>{status.line}</strong></span>}
|
||||
{status.column !== undefined && <span>Column <strong>{status.column}</strong></span>}
|
||||
|
|
@ -527,8 +527,8 @@ export default function XmlFormatterPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Input XML</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Input XML</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({inputLineCount} {inputLineCount === 1 ? "line" : "lines"}, {input.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -542,14 +542,14 @@ export default function XmlFormatterPage() {
|
|||
// Permission denied
|
||||
}
|
||||
}}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-600 hover:bg-[#111] cursor-pointer"
|
||||
title="Paste from clipboard"
|
||||
>
|
||||
Paste
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setInput("")}
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-red-600 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-red-400 cursor-pointer"
|
||||
className="rounded px-2 py-1 text-xs font-medium text-gray-500 hover:bg-[#111] hover:text-red-400 cursor-pointer"
|
||||
title="Clear input"
|
||||
>
|
||||
Clear
|
||||
|
|
@ -562,7 +562,7 @@ export default function XmlFormatterPage() {
|
|||
placeholder="Paste raw or unformatted XML here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-white p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-[#0a0a0a] p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -570,8 +570,8 @@ export default function XmlFormatterPage() {
|
|||
<div className="flex flex-col">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-gray-200">Formatted XML Output</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="text-sm font-semibold text-gray-300">Formatted XML Output</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
({outputLineCount} {outputLineCount === 1 ? "line" : "lines"}, {output.length} chars)
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -579,7 +579,7 @@ export default function XmlFormatterPage() {
|
|||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!output && !input}
|
||||
className="inline-flex items-center gap-1 rounded px-2.5 py-1 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:text-gray-300 dark:hover:bg-gray-800 cursor-pointer"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-medium text-gray-400 hover:bg-[#111] disabled:opacity-40 cursor-pointer"
|
||||
title="Download as XML file"
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -590,7 +590,7 @@ export default function XmlFormatterPage() {
|
|||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={!output && !input}
|
||||
className="inline-flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
className="inline-flex items-center gap-1 bg-blue-600 px-3 py-1 text-xs font-semibold text-white shadow-xs transition-colors hover:bg-blue-700 disabled:opacity-40 cursor-pointer"
|
||||
title="Copy XML output"
|
||||
>
|
||||
{copied ? (
|
||||
|
|
@ -622,29 +622,29 @@ export default function XmlFormatterPage() {
|
|||
placeholder="Formatted XML output will appear here..."
|
||||
rows={18}
|
||||
spellCheck={false}
|
||||
className="w-full flex-1 resize-y rounded-lg border border-gray-300 bg-gray-50 p-3 font-mono text-sm leading-relaxed text-gray-900 shadow-xs focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-900/80 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
className="w-full flex-1 resize-y border border-[#1a1a1a] bg-black p-3 font-mono text-sm leading-relaxed text-gray-200 shadow-xs focus:border-blue-500 focus:ring-1 focus:ring-blue-900 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature Highlights & Guide */}
|
||||
<div className="mt-8 rounded-xl border border-gray-200 bg-gray-50/70 p-5 text-sm text-gray-600 dark:border-gray-800 dark:bg-gray-800/30 dark:text-gray-400">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100">About XML Formatter & Validator</h3>
|
||||
<div className="mt-8 border border-[#1a1a1a] bg-black/70 p-5 text-sm text-gray-600">
|
||||
<h3 className="font-semibold text-gray-200">About XML Formatter & Validator</h3>
|
||||
<div className="mt-3 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">✨ Proper Tree Serialization</p>
|
||||
<p className="font-medium text-gray-300">✨ Proper Tree Serialization</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Indents elements, handles namespaces, attributes, comments, processing instructions, and CDATA blocks.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔍 Real-time DOM Validation</p>
|
||||
<p className="font-medium text-gray-300">🔍 Real-time DOM Validation</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Validates XML well-formedness and points directly to unclosed tags, mismatching brackets, or invalid characters.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800 dark:text-gray-200">🔒 100% Client-Side</p>
|
||||
<p className="font-medium text-gray-300">🔒 100% Client-Side</p>
|
||||
<p className="mt-1 text-xs leading-normal">
|
||||
Uses the browser's native DOMParser engine. No XML payload ever leaves your computer.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function Header() {
|
|||
}}
|
||||
onFocus={() => setShowResults(true)}
|
||||
onBlur={() => setTimeout(() => setShowResults(false), 150)}
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] pl-10 pr-3 py-2 text-sm text-gray-300 placeholder-[#525252] outline-none focus:border-emerald-900 transition-colors"
|
||||
className="w-full border border-[#1a1a1a] bg-[#0a0a0a] pl-10 pr-3 py-2 text-sm text-gray-300 placeholder-[#525252] outline-none focus:border-blue-900 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ export default function Header() {
|
|||
<Link
|
||||
key={tool.slug}
|
||||
href={`/tools/${tool.slug}`}
|
||||
className="flex items-center gap-3 border-b border-[#111] px-4 py-2.5 text-sm text-gray-400 hover:bg-[#111] hover:text-emerald-400 transition-colors"
|
||||
className="flex items-center gap-3 border-b border-[#111] px-4 py-2.5 text-sm text-gray-400 hover:bg-[#111] hover:text-blue-400 transition-colors"
|
||||
onClick={() => {
|
||||
setQuery("");
|
||||
setShowResults(false);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function ToolCard({ tool }: { tool: Tool }) {
|
|||
>
|
||||
<span className="text-lg mt-0.5 grayscale group-hover:grayscale-0 transition-all">{tool.icon}</span>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-300 group-hover:text-emerald-400 transition-colors">
|
||||
<div className="text-sm font-medium text-gray-300 group-hover:text-blue-400 transition-colors">
|
||||
{tool.name}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-[#525252] leading-relaxed">{tool.description}</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function ToolLayout({ title, description, children }: ToolLayoutP
|
|||
return (
|
||||
<div className="relative z-10 mx-auto max-w-5xl px-6 py-8">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="h-px w-4 bg-emerald-900"></div>
|
||||
<div className="h-px w-4 bg-blue-900"></div>
|
||||
<h1 className="text-lg font-semibold text-white">{title}</h1>
|
||||
</div>
|
||||
<p className="mb-5 text-sm text-[#525252] ml-6">{description}</p>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue