Update UI design to premium violet palette, tweak hero section, and make search bar outline visible
This commit is contained in:
parent
eb8a302222
commit
c159ad4f37
11 changed files with 247 additions and 152 deletions
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
|
|
@ -8,6 +8,8 @@
|
||||||
"name": "paperjet",
|
"name": "paperjet",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/outfit": "^5.2.8",
|
||||||
|
"@fontsource/plus-jakarta-sans": "^5.2.8",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"fabric": "^7.4.0",
|
"fabric": "^7.4.0",
|
||||||
|
|
@ -658,6 +660,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/outfit": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/outfit/-/outfit-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-rXC6g0MqD7cOBjht0bMqc43qM6lRqDLML9KXsmg9uykz0wLQhy8Z/ajrMG6iyoT3NJR+MYgU+OEHp7uHoTb+Yw==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/plus-jakarta-sans": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/plus-jakarta-sans/-/plus-jakarta-sans-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-P5qE49fqdeD+7DXH1KBxmMPlB17LTz1zvBhFH0tFzfnYTKVJVyb0pR6plh0ZGXxcB+Oayb54FZZw3V42/DawTw==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/outfit": "^5.2.8",
|
||||||
|
"@fontsource/plus-jakarta-sans": "^5.2.8",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"date-fns": "^4.4.0",
|
"date-fns": "^4.4.0",
|
||||||
"fabric": "^7.4.0",
|
"fabric": "^7.4.0",
|
||||||
|
|
|
||||||
|
|
@ -20,25 +20,28 @@ export const LoginForm = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md p-8 space-y-8 bg-white/70 dark:bg-gray-800/70 backdrop-blur-xl rounded-2xl shadow-2xl border border-gray-100 dark:border-gray-700 transition-all">
|
<div className="w-full max-w-md p-10 space-y-8 bg-white/80 dark:bg-gray-800/80 backdrop-blur-2xl rounded-[2rem] shadow-2xl shadow-accent-500/10 border border-white/50 dark:border-gray-700 transition-all">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white mb-2">
|
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-accent-400 to-accent-600 rounded-2xl flex items-center justify-center shadow-lg shadow-accent-500/30 mb-6 transform -rotate-6 hover:rotate-0 transition-transform duration-300">
|
||||||
|
<span className="text-white font-heading font-bold text-3xl leading-none">P</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-4xl font-heading font-bold tracking-tight text-gray-900 dark:text-white mb-2">
|
||||||
Welcome back
|
Welcome back
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-base text-gray-500 dark:text-gray-400 font-medium">
|
||||||
Enter your master password to unlock your library
|
Enter your master password to unlock your library
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2 ml-1">
|
||||||
Password
|
Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white/50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all placeholder-gray-400 dark:placeholder-gray-500"
|
className="w-full px-5 py-4 rounded-full border-2 border-transparent bg-white/60 dark:bg-gray-700/60 text-gray-900 dark:text-white focus:bg-white focus:ring-4 focus:ring-accent-500/20 focus:border-accent-400 outline-none transition-all placeholder-gray-400 dark:placeholder-gray-500 shadow-inner text-lg font-medium"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
|
@ -47,7 +50,7 @@ export const LoginForm = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-3 text-sm text-red-600 bg-red-50 dark:bg-red-900/30 dark:text-red-400 rounded-lg animate-pulse">
|
<div className="p-4 text-sm font-semibold text-red-600 bg-red-50 dark:bg-red-900/30 dark:text-red-400 rounded-2xl animate-in fade-in slide-in-from-top-2 border border-red-100 dark:border-red-800/50">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -55,10 +58,10 @@ export const LoginForm = () => {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading || !password}
|
disabled={isLoading || !password}
|
||||||
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-[0.98]"
|
className="w-full flex justify-center py-4 px-6 border-2 border-transparent rounded-full shadow-lg text-lg font-bold text-white bg-gradient-to-r from-accent-500 to-accent-600 hover:from-accent-600 hover:to-accent-700 focus:outline-none focus:ring-4 focus:ring-accent-500/30 disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-[0.98] hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
<svg className="animate-spin -ml-1 mr-3 h-6 w-6 text-white" fill="none" viewBox="0 0 24 24">
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,17 @@ export const SetupForm = () => {
|
||||||
const displayError = localError || error
|
const displayError = localError || error
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md p-8 space-y-8 bg-white/70 dark:bg-gray-800/70 backdrop-blur-xl rounded-2xl shadow-2xl border border-gray-100 dark:border-gray-700 transition-all">
|
<div className="w-full max-w-md p-10 space-y-8 bg-white/80 dark:bg-gray-800/80 backdrop-blur-2xl rounded-[2rem] shadow-2xl shadow-accent-500/10 border border-white/50 dark:border-gray-700 transition-all">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mx-auto w-16 h-16 bg-indigo-100 dark:bg-indigo-900/50 rounded-full flex items-center justify-center mb-6">
|
<div className="mx-auto w-16 h-16 bg-gradient-to-br from-accent-400 to-accent-600 rounded-2xl flex items-center justify-center shadow-lg shadow-accent-500/30 mb-6 transform -rotate-6 hover:rotate-0 transition-transform duration-300">
|
||||||
<svg className="w-8 h-8 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8V7a4 4 0 00-8 0v4h8z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8V7a4 4 0 00-8 0v4h8z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-extrabold tracking-tight text-gray-900 dark:text-white mb-2">
|
<h2 className="text-4xl font-heading font-bold tracking-tight text-gray-900 dark:text-white mb-2">
|
||||||
Set up PaperJet
|
Set up PaperJet
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
<p className="text-base text-gray-500 dark:text-gray-400 font-medium">
|
||||||
Create a master password to secure your documents. Make sure you remember it!
|
Create a master password to secure your documents. Make sure you remember it!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -52,13 +52,13 @@ export const SetupForm = () => {
|
||||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2 ml-1">
|
||||||
Master Password
|
Master Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white/50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all"
|
className="w-full px-5 py-4 rounded-full border-2 border-transparent bg-white/60 dark:bg-gray-700/60 text-gray-900 dark:text-white focus:bg-white focus:ring-4 focus:ring-accent-500/20 focus:border-accent-400 outline-none transition-all placeholder-gray-400 dark:placeholder-gray-500 shadow-inner text-lg font-medium"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
|
@ -66,13 +66,13 @@ export const SetupForm = () => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<label className="block text-sm font-bold text-gray-700 dark:text-gray-300 mb-2 ml-1">
|
||||||
Confirm Password
|
Confirm Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
required
|
required
|
||||||
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white/50 dark:bg-gray-700/50 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all"
|
className="w-full px-5 py-4 rounded-full border-2 border-transparent bg-white/60 dark:bg-gray-700/60 text-gray-900 dark:text-white focus:bg-white focus:ring-4 focus:ring-accent-500/20 focus:border-accent-400 outline-none transition-all placeholder-gray-400 dark:placeholder-gray-500 shadow-inner text-lg font-medium"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
|
@ -82,7 +82,7 @@ export const SetupForm = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{displayError && (
|
{displayError && (
|
||||||
<div className="p-3 text-sm text-red-600 bg-red-50 dark:bg-red-900/30 dark:text-red-400 rounded-lg animate-pulse">
|
<div className="p-4 text-sm font-semibold text-red-600 bg-red-50 dark:bg-red-900/30 dark:text-red-400 rounded-2xl animate-in fade-in slide-in-from-top-2 border border-red-100 dark:border-red-800/50">
|
||||||
{displayError}
|
{displayError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -90,10 +90,10 @@ export const SetupForm = () => {
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading || !password || !confirmPassword}
|
disabled={isLoading || !password || !confirmPassword}
|
||||||
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-xl shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-[0.98]"
|
className="w-full flex justify-center py-4 px-6 border-2 border-transparent rounded-full shadow-lg text-lg font-bold text-white bg-gradient-to-r from-accent-500 to-accent-600 hover:from-accent-600 hover:to-accent-700 focus:outline-none focus:ring-4 focus:ring-accent-500/30 disabled:opacity-50 disabled:cursor-not-allowed transition-all active:scale-[0.98] hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
|
<svg className="animate-spin -ml-1 mr-3 h-6 w-6 text-white" fill="none" viewBox="0 0 24 24">
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export const ContextMenu = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className="fixed z-50 w-48 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 py-2 overflow-hidden animate-in fade-in zoom-in duration-100"
|
className="fixed z-50 w-56 bg-white/90 dark:bg-gray-800/90 backdrop-blur-xl rounded-2xl shadow-xl shadow-accent-500/10 border border-white/50 dark:border-gray-700 py-2 overflow-hidden animate-in fade-in zoom-in duration-150"
|
||||||
style={{ left: safeX, top: safeY }}
|
style={{ left: safeX, top: safeY }}
|
||||||
onContextMenu={(e) => e.preventDefault()}
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
|
|
@ -56,20 +56,20 @@ export const ContextMenu = ({
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onOpen(); onClose(); }}
|
onClick={() => { onOpen(); onClose(); }}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
className="w-full text-left px-5 py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-accent-50 dark:hover:bg-accent-900/30 hover:text-accent-600 dark:hover:text-accent-400 transition-colors"
|
||||||
>
|
>
|
||||||
Open in Editor
|
Open in Editor
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onRename(); onClose(); }}
|
onClick={() => { onRename(); onClose(); }}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-indigo-50 dark:hover:bg-indigo-900/30 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
className="w-full text-left px-5 py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-accent-50 dark:hover:bg-accent-900/30 hover:text-accent-600 dark:hover:text-accent-400 transition-colors"
|
||||||
>
|
>
|
||||||
Rename...
|
Rename...
|
||||||
</button>
|
</button>
|
||||||
<div className="h-px bg-gray-200 dark:bg-gray-700 my-1 mx-2" />
|
<div className="h-px bg-gray-200 dark:bg-gray-700 my-1.5 mx-3" />
|
||||||
<button
|
<button
|
||||||
onClick={() => { onDelete(); onClose(); }}
|
onClick={() => { onDelete(); onClose(); }}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors"
|
className="w-full text-left px-5 py-2.5 text-sm font-bold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors"
|
||||||
>
|
>
|
||||||
Move to Trash
|
Move to Trash
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -80,14 +80,14 @@ export const ContextMenu = ({
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => { onRestore(); onClose(); }}
|
onClick={() => { onRestore(); onClose(); }}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-emerald-50 dark:hover:bg-emerald-900/30 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
|
className="w-full text-left px-5 py-2.5 text-sm font-bold text-gray-700 dark:text-gray-200 hover:bg-emerald-50 dark:hover:bg-emerald-900/30 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
|
||||||
>
|
>
|
||||||
Restore Document
|
Restore Document
|
||||||
</button>
|
</button>
|
||||||
<div className="h-px bg-gray-200 dark:bg-gray-700 my-1 mx-2" />
|
<div className="h-px bg-gray-200 dark:bg-gray-700 my-1.5 mx-3" />
|
||||||
<button
|
<button
|
||||||
onClick={() => { onHardDelete(); onClose(); }}
|
onClick={() => { onHardDelete(); onClose(); }}
|
||||||
className="w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors"
|
className="w-full text-left px-5 py-2.5 text-sm font-bold text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors"
|
||||||
>
|
>
|
||||||
Delete Permanently
|
Delete Permanently
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -57,25 +57,21 @@ export const DocumentGrid = ({ isTrash }: { isTrash: boolean }) => {
|
||||||
<div
|
<div
|
||||||
key={doc.id}
|
key={doc.id}
|
||||||
onContextMenu={(e) => handleContextMenu(e, doc.id)}
|
onContextMenu={(e) => handleContextMenu(e, doc.id)}
|
||||||
className={`group relative flex flex-col bg-white dark:bg-gray-800 rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300 border-2 ${
|
className={`group relative flex flex-col bg-white dark:bg-gray-800 rounded-3xl overflow-hidden shadow-sm hover:shadow-xl hover:shadow-accent-500/10 transition-all duration-300 border border-gray-200 dark:border-gray-700 hover:-translate-y-1 ${
|
||||||
isSelected ? 'border-indigo-500 shadow-md ring-2 ring-indigo-500/20' : 'border-transparent hover:border-indigo-200 dark:hover:border-indigo-800'
|
isSelected ? 'border-accent-500 shadow-md ring-2 ring-accent-500/50 scale-[1.02]' : 'hover:border-accent-300 dark:hover:border-accent-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
{isSelected && (
|
||||||
className="absolute top-3 left-3 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
|
<div className="absolute top-3 left-3 z-20 w-6 h-6 bg-accent-500 rounded-full flex items-center justify-center shadow-md ring-2 ring-white">
|
||||||
style={{ opacity: isSelected ? 1 : undefined }}
|
<svg className="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7" />
|
||||||
<input
|
</svg>
|
||||||
type="checkbox"
|
|
||||||
checked={isSelected}
|
|
||||||
onChange={() => toggleSelection(doc.id)}
|
|
||||||
className="w-5 h-5 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer shadow-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={`/editor/${doc.id}`}
|
to={`/editor/${doc.id}`}
|
||||||
className="relative aspect-[3/4] bg-gray-100 dark:bg-gray-900 overflow-hidden"
|
className="relative aspect-[3/4] bg-gray-50/50 dark:bg-gray-900/50 overflow-hidden"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (isTrash) e.preventDefault() // disable link in trash
|
if (isTrash) e.preventDefault() // disable link in trash
|
||||||
}}
|
}}
|
||||||
|
|
@ -83,29 +79,34 @@ export const DocumentGrid = ({ isTrash }: { isTrash: boolean }) => {
|
||||||
<img
|
<img
|
||||||
src={thumbnailUrl}
|
src={thumbnailUrl}
|
||||||
alt={doc.title}
|
alt={doc.title}
|
||||||
className={`w-full h-full object-cover transition-transform duration-500 group-hover:scale-105 ${isTrash ? 'grayscale opacity-70' : ''}`}
|
className={`w-full h-full object-contain transition-transform duration-500 group-hover:scale-105 p-3 ${isTrash ? 'grayscale opacity-70' : ''}`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
// Fallback if no thumbnail
|
// Fallback if no thumbnail
|
||||||
(e.target as HTMLImageElement).src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="%239ca3af" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>'
|
const target = e.target as HTMLImageElement;
|
||||||
;(e.target as HTMLImageElement).className = 'w-1/2 h-1/2 absolute top-1/4 left-1/4 opacity-30 object-contain'
|
target.onerror = null;
|
||||||
|
target.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fill="none" stroke="%23cbd5e1" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="9" x2="15" y2="15"></line><line x1="15" y1="9" x2="9" y2="15"></line></svg>';
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-black/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
|
||||||
|
{/* Hover overlay */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-gray-900/40 via-transparent to-gray-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="p-4 flex flex-col">
|
<div className="p-4 flex flex-col gap-1.5 border-t border-gray-100 dark:border-gray-700 bg-white dark:bg-gray-800">
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white truncate" title={doc.title}>
|
<h3 className="text-sm font-bold text-gray-900 dark:text-white truncate" title={doc.title}>
|
||||||
{doc.title}
|
{doc.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-xs font-semibold text-gray-500 dark:text-gray-400">
|
||||||
{formatDistanceToNow(new Date(doc.updated_at), { addSuffix: true })}
|
{formatDistanceToNow(new Date(doc.updated_at), { addSuffix: true })}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-gray-400 dark:text-gray-500 mt-0.5">
|
<p className="text-[10px] font-bold text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded-md">
|
||||||
{(doc.size_bytes / 1024 / 1024).toFixed(1)} MB
|
{(doc.size_bytes / 1024 / 1024).toFixed(1)} MB
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,59 +28,60 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
||||||
const allSelected = documents.length > 0 && selectedIds.size === documents.length
|
const allSelected = documents.length > 0 && selectedIds.size === documents.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-0 z-40 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-800">
|
<header className="sticky top-0 z-40 w-full backdrop-blur-2xl bg-white/70 dark:bg-gray-900/70 border-b border-gray-200 dark:border-gray-800 transition-all shadow-sm">
|
||||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between gap-4">
|
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between gap-6">
|
||||||
|
|
||||||
{/* Left: Branding & Tabs */}
|
{/* Left: Logo Area */}
|
||||||
<div className="flex items-center gap-8">
|
<div className="flex items-center gap-4 min-w-max flex-shrink-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="w-10 h-10 bg-gradient-to-br from-accent-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-accent-500/20 transform -rotate-3 hover:rotate-0 transition-transform">
|
||||||
<div className="w-8 h-8 bg-indigo-600 rounded-lg flex items-center justify-center shadow-lg shadow-indigo-500/30">
|
<span className="text-white font-heading font-bold text-xl leading-none">P</span>
|
||||||
<span className="text-white font-bold text-xl leading-none">P</span>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white hidden sm:block">PaperJet</h1>
|
<h1 className="text-2xl font-heading font-bold text-gray-900 dark:text-white tracking-tight hidden lg:block">
|
||||||
|
PaperJet
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex items-center gap-1 bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
|
{/* Center: Navigation */}
|
||||||
|
<nav className="flex items-center p-1.5 bg-white/50 dark:bg-gray-800/50 rounded-full border border-gray-200/50 dark:border-gray-700 shadow-inner">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTabChange('library')}
|
onClick={() => handleTabChange('library')}
|
||||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${
|
className={`px-6 py-2 text-sm font-bold rounded-full transition-all duration-300 ${
|
||||||
currentTab === 'library'
|
currentTab === 'library'
|
||||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
? 'bg-white dark:bg-gray-700 text-accent-600 dark:text-accent-400 shadow-md transform scale-105'
|
||||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Library
|
Library
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTabChange('trash')}
|
onClick={() => handleTabChange('trash')}
|
||||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${
|
className={`px-6 py-2 text-sm font-bold rounded-full transition-all duration-300 ${
|
||||||
currentTab === 'trash'
|
currentTab === 'trash'
|
||||||
? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-white shadow-sm'
|
? 'bg-white dark:bg-gray-700 text-accent-600 dark:text-accent-400 shadow-md transform scale-105'
|
||||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50/50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Trash
|
Trash
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: Search & Actions */}
|
{/* Right: Actions */}
|
||||||
<div className="flex items-center gap-4 flex-1 justify-end">
|
<div className="flex items-center gap-4 flex-1 justify-end">
|
||||||
{isSelectionMode ? (
|
{isSelectionMode ? (
|
||||||
<div className="flex items-center gap-3 bg-indigo-50 dark:bg-indigo-900/30 px-4 py-1.5 rounded-full border border-indigo-100 dark:border-indigo-800">
|
<div className="flex items-center gap-3 bg-white/90 dark:bg-gray-800/90 px-5 py-2 rounded-full border border-accent-200 dark:border-accent-800 shadow-sm animate-in fade-in slide-in-from-top-2">
|
||||||
<span className="text-sm font-medium text-indigo-700 dark:text-indigo-300">
|
<span className="text-sm font-bold text-accent-700 dark:text-accent-300">
|
||||||
{selectedIds.size} selected
|
{selectedIds.size} selected
|
||||||
</span>
|
</span>
|
||||||
<div className="h-4 w-px bg-indigo-200 dark:bg-indigo-700"></div>
|
<div className="h-5 w-px bg-accent-200 dark:bg-accent-800/50"></div>
|
||||||
|
|
||||||
<button onClick={allSelected ? clearSelection : selectAll} className="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-200 font-medium">
|
<button onClick={allSelected ? clearSelection : selectAll} className="text-sm text-accent-600 dark:text-accent-400 hover:text-accent-800 dark:hover:text-accent-200 font-bold transition-colors">
|
||||||
{allSelected ? 'Deselect All' : 'Select All'}
|
{allSelected ? 'Deselect All' : 'Select All'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{currentTab === 'library' ? (
|
{currentTab === 'library' ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => bulkDelete()}
|
onClick={() => bulkDelete()}
|
||||||
className="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 font-medium px-2 py-1 hover:bg-red-50 dark:hover:bg-red-900/30 rounded"
|
className="text-sm text-red-600 dark:text-red-400 hover:text-white hover:bg-red-500 font-bold px-3 py-1.5 rounded-full transition-colors ml-2"
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -88,13 +89,13 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => bulkRestore()}
|
onClick={() => bulkRestore()}
|
||||||
className="text-sm text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 font-medium px-2 py-1 hover:bg-green-50 dark:hover:bg-green-900/30 rounded"
|
className="text-sm text-emerald-600 dark:text-emerald-400 hover:text-white hover:bg-emerald-500 font-bold px-3 py-1.5 rounded-full transition-colors ml-2"
|
||||||
>
|
>
|
||||||
Restore
|
Restore
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => bulkDelete(true)}
|
onClick={() => bulkDelete(true)}
|
||||||
className="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 font-medium px-2 py-1 hover:bg-red-50 dark:hover:bg-red-900/30 rounded"
|
className="text-sm text-red-600 dark:text-red-400 hover:text-white hover:bg-red-500 font-bold px-3 py-1.5 rounded-full transition-colors ml-1"
|
||||||
>
|
>
|
||||||
Delete Forever
|
Delete Forever
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -104,10 +105,10 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{currentTab === 'library' && (
|
{currentTab === 'library' && (
|
||||||
<form onSubmit={handleSearch} className="relative hidden md:block max-w-md w-full">
|
<form onSubmit={handleSearch} className="relative hidden md:block max-w-[240px] w-full group">
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none transition-colors group-focus-within:text-accent-500 text-gray-400">
|
||||||
<svg className="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|
@ -115,7 +116,7 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
placeholder="Search documents..."
|
placeholder="Search documents..."
|
||||||
className="block w-full pl-10 pr-3 py-2 border border-gray-200 dark:border-gray-700 rounded-xl leading-5 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent sm:text-sm transition-all"
|
className="block w-full pl-11 pr-4 py-2 border border-gray-300 dark:border-gray-600 bg-white/80 dark:bg-gray-800/80 rounded-full leading-5 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-none focus:bg-white focus:border-accent-400 focus:ring-4 focus:ring-accent-500/20 sm:text-sm transition-all shadow-inner font-medium"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
@ -123,14 +124,14 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
||||||
{currentTab === 'trash' && (
|
{currentTab === 'trash' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => emptyTrash()}
|
onClick={() => emptyTrash()}
|
||||||
className="text-sm font-medium text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors"
|
className="text-sm font-bold text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 transition-colors px-4 py-2 rounded-full hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||||
>
|
>
|
||||||
Empty Trash
|
Empty Trash
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentTab === 'library' && (
|
{currentTab === 'library' && (
|
||||||
<label className="cursor-pointer inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-medium rounded-xl shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 transition-all active:scale-95">
|
<label className="cursor-pointer hidden sm:flex items-center justify-center px-6 py-2 border-2 border-transparent text-sm font-bold rounded-full text-white bg-gradient-to-r from-accent-500 to-accent-600 hover:from-accent-600 hover:to-accent-700 focus:outline-none focus:ring-4 focus:ring-accent-500/30 transition-all shadow-lg shadow-accent-500/25 hover:shadow-accent-500/40 hover:-translate-y-0.5 active:translate-y-0">
|
||||||
<span>Upload PDF</span>
|
<span>Upload PDF</span>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
|
@ -138,8 +139,6 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
||||||
multiple
|
multiple
|
||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
// We'll dispatch a custom event that UploadArea listens to, or just click its input
|
|
||||||
// For simplicity, find the hidden file input in UploadArea and click it
|
|
||||||
const uploader = document.querySelector('input[type="file"][multiple]') as HTMLInputElement
|
const uploader = document.querySelector('input[type="file"][multiple]') as HTMLInputElement
|
||||||
if (uploader && uploader !== e.target) {
|
if (uploader && uploader !== e.target) {
|
||||||
uploader.files = e.target.files
|
uploader.files = e.target.files
|
||||||
|
|
@ -153,17 +152,17 @@ export const LibraryHeader = ({ currentTab, onTabChange }: { currentTab: 'librar
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="p-2 text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
|
className="p-2.5 text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-700 rounded-full transition-all shadow-sm border border-gray-200/50 dark:border-gray-700"
|
||||||
title="Logout"
|
title="Logout"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,13 +69,15 @@ export const UploadArea = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|
||||||
{/* Invisible overlay that appears when dragging to prevent flickering */}
|
{/* Invisible overlay that appears when dragging to prevent flickering */}
|
||||||
{isDragging && (
|
{isDragging && (
|
||||||
<div className="absolute inset-0 z-50 bg-indigo-500/10 dark:bg-indigo-900/20 backdrop-blur-sm border-4 border-dashed border-indigo-500 dark:border-indigo-400 rounded-3xl m-4 flex items-center justify-center transition-all pointer-events-none">
|
<div className="absolute inset-0 z-50 bg-accent-500/10 dark:bg-accent-900/20 backdrop-blur-md border-4 border-dashed border-accent-400 dark:border-accent-500 rounded-3xl m-4 flex items-center justify-center transition-all pointer-events-none">
|
||||||
<div className="bg-white/90 dark:bg-gray-800/90 px-8 py-6 rounded-2xl shadow-2xl flex flex-col items-center">
|
<div className="bg-white/95 dark:bg-gray-800/95 px-10 py-8 rounded-3xl shadow-2xl shadow-accent-500/20 flex flex-col items-center transform scale-105 transition-transform duration-300 border border-white/50">
|
||||||
<svg className="w-16 h-16 text-indigo-500 mb-4 animate-bounce" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div className="w-20 h-20 bg-gradient-to-br from-accent-100 to-accent-200 dark:from-accent-800 dark:to-accent-900 rounded-full flex items-center justify-center mb-6 shadow-inner">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
<svg className="w-10 h-10 text-accent-600 dark:text-accent-300 animate-bounce" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||||
</svg>
|
</svg>
|
||||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">Drop PDFs here</h3>
|
</div>
|
||||||
<p className="text-gray-500 dark:text-gray-400 mt-2">Release to upload to your library</p>
|
<h3 className="text-3xl font-heading font-bold text-gray-900 dark:text-white tracking-tight">Drop PDFs here</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mt-2 font-medium">Release to upload to your library</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,12 @@
|
||||||
|
@import "@fontsource/outfit/400.css";
|
||||||
|
@import "@fontsource/outfit/500.css";
|
||||||
|
@import "@fontsource/outfit/600.css";
|
||||||
|
@import "@fontsource/outfit/700.css";
|
||||||
|
@import "@fontsource/plus-jakarta-sans/400.css";
|
||||||
|
@import "@fontsource/plus-jakarta-sans/500.css";
|
||||||
|
@import "@fontsource/plus-jakarta-sans/600.css";
|
||||||
|
@import "@fontsource/plus-jakarta-sans/700.css";
|
||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* Force light mode by requiring a .dark class that we never add */
|
/* Force light mode by requiring a .dark class that we never add */
|
||||||
|
|
@ -6,44 +15,44 @@
|
||||||
/*
|
/*
|
||||||
* PaperJet Design Tokens
|
* PaperJet Design Tokens
|
||||||
*
|
*
|
||||||
* Sejda-inspired: bright, airy, light mode with generous whitespace,
|
* Canva-inspired: Playful but professional. Violet/Indigo accents, dot-grid background.
|
||||||
* a restrained palette (one accent), soft shadows, rounded corners.
|
|
||||||
*/
|
*/
|
||||||
@theme {
|
@theme {
|
||||||
/* Accent color — refined blue */
|
/* Premium Violet/Indigo Accent */
|
||||||
--color-accent-50: oklch(0.97 0.02 250);
|
--color-accent-50: #f5f3ff;
|
||||||
--color-accent-100: oklch(0.93 0.04 250);
|
--color-accent-100: #ede9fe;
|
||||||
--color-accent-200: oklch(0.87 0.08 250);
|
--color-accent-200: #ddd6fe;
|
||||||
--color-accent-300: oklch(0.78 0.12 250);
|
--color-accent-300: #c4b5fd;
|
||||||
--color-accent-400: oklch(0.68 0.16 250);
|
--color-accent-400: #a855f7; /* Purple */
|
||||||
--color-accent-500: oklch(0.58 0.19 250);
|
--color-accent-500: #8b5cf6; /* Violet */
|
||||||
--color-accent-600: oklch(0.50 0.19 250);
|
--color-accent-600: #7c3aed;
|
||||||
--color-accent-700: oklch(0.43 0.17 250);
|
--color-accent-700: #6d28d9;
|
||||||
--color-accent-800: oklch(0.37 0.14 250);
|
--color-accent-800: #5b21b6;
|
||||||
--color-accent-900: oklch(0.30 0.10 250);
|
--color-accent-900: #4c1d95;
|
||||||
|
|
||||||
/* Neutrals — warm gray */
|
/* Neutrals — clean cool gray */
|
||||||
--color-neutral-50: oklch(0.985 0.002 250);
|
--color-neutral-50: #f8fafc;
|
||||||
--color-neutral-100: oklch(0.965 0.004 250);
|
--color-neutral-100: #f1f5f9;
|
||||||
--color-neutral-200: oklch(0.925 0.006 250);
|
--color-neutral-200: #e2e8f0;
|
||||||
--color-neutral-300: oklch(0.870 0.008 250);
|
--color-neutral-300: #cbd5e1;
|
||||||
--color-neutral-400: oklch(0.700 0.010 250);
|
--color-neutral-400: #94a3b8;
|
||||||
--color-neutral-500: oklch(0.550 0.012 250);
|
--color-neutral-500: #64748b;
|
||||||
--color-neutral-600: oklch(0.440 0.012 250);
|
--color-neutral-600: #475569;
|
||||||
--color-neutral-700: oklch(0.370 0.012 250);
|
--color-neutral-700: #334155;
|
||||||
--color-neutral-800: oklch(0.270 0.010 250);
|
--color-neutral-800: #1e293b;
|
||||||
--color-neutral-900: oklch(0.180 0.008 250);
|
--color-neutral-900: #0f172a;
|
||||||
|
|
||||||
/* Semantic */
|
/* Semantic */
|
||||||
--color-success: oklch(0.60 0.16 145);
|
--color-success: #10b981;
|
||||||
--color-warning: oklch(0.75 0.16 65);
|
--color-warning: #f59e0b;
|
||||||
--color-error: oklch(0.58 0.20 25);
|
--color-error: #ef4444;
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
--font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
--font-sans: 'Plus Jakarta Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
--font-heading: 'Outfit', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
|
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
--duration-fast: 150ms;
|
--duration-fast: 150ms;
|
||||||
--duration-normal: 250ms;
|
--duration-normal: 250ms;
|
||||||
|
|
@ -61,18 +70,27 @@ html {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Modern Dot Grid Background
|
||||||
|
Very clean, provides texture without overwhelming the content.
|
||||||
|
*/
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--color-neutral-50);
|
|
||||||
color: var(--color-neutral-800);
|
color: var(--color-neutral-800);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
background-image: radial-gradient(#cbd5e1 1px, transparent 1px);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth scrolling */
|
/* Headings use the geometric font */
|
||||||
html {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
scroll-behavior: smooth;
|
font-family: var(--font-heading);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus ring utility */
|
/* Focus ring utility */
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,64 @@ export function HomePage() {
|
||||||
}, [currentTab, fetchDocuments, fetchTrash])
|
}, [currentTab, fetchDocuments, fetchTrash])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-950 transition-colors">
|
<div className="min-h-screen transition-colors">
|
||||||
<LibraryHeader currentTab={currentTab} onTabChange={setCurrentTab} />
|
<LibraryHeader currentTab={currentTab} onTabChange={setCurrentTab} />
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-6 py-8">
|
<main className="max-w-7xl mx-auto px-6 py-8">
|
||||||
{currentTab === 'library' ? (
|
{currentTab === 'library' ? (
|
||||||
<UploadArea>
|
<UploadArea>
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="relative overflow-hidden rounded-[2.5rem] bg-gradient-to-br from-accent-600 to-indigo-900 text-white mb-8 shadow-2xl shadow-accent-900/20">
|
||||||
|
{/* Decorative background shapes */}
|
||||||
|
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-96 h-96 bg-white/10 blur-3xl rounded-full mix-blend-overlay"></div>
|
||||||
|
<div className="absolute bottom-0 left-0 -ml-20 -mb-20 w-80 h-80 bg-accent-400/20 blur-3xl rounded-full mix-blend-overlay"></div>
|
||||||
|
|
||||||
|
<div className="relative px-8 py-10 sm:px-12 sm:py-14 flex flex-col md:flex-row items-center justify-between gap-8">
|
||||||
|
<div className="max-w-xl">
|
||||||
|
<h2 className="text-3xl sm:text-4xl font-heading font-bold tracking-tight mb-3">
|
||||||
|
What will you edit today?
|
||||||
|
</h2>
|
||||||
|
<p className="text-base sm:text-lg text-accent-100 font-medium mb-6 leading-relaxed">
|
||||||
|
Upload any PDF to instantly annotate, sign, and modify it. Drop your files right here to get started.
|
||||||
|
</p>
|
||||||
|
<label className="cursor-pointer inline-flex items-center justify-center px-6 py-3 text-base font-bold rounded-full text-accent-900 bg-white hover:bg-gray-50 focus:outline-none focus:ring-4 focus:ring-white/30 transition-all shadow-xl hover:-translate-y-1 active:translate-y-0">
|
||||||
|
<svg className="w-5 h-5 mr-2 text-accent-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||||
|
</svg>
|
||||||
|
<span>Browse Files</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const uploader = document.querySelector('input[type="file"][multiple]') as HTMLInputElement
|
||||||
|
if (uploader && uploader !== e.target) {
|
||||||
|
uploader.files = e.target.files
|
||||||
|
const event = new Event('change', { bubbles: true })
|
||||||
|
uploader.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visual Graphic */}
|
||||||
|
<div className="hidden md:block w-72 h-72 relative">
|
||||||
|
<div className="absolute inset-0 bg-white/10 backdrop-blur-md rounded-2xl border border-white/20 transform rotate-6 shadow-2xl"></div>
|
||||||
|
<div className="absolute inset-0 bg-white/20 backdrop-blur-md rounded-2xl border border-white/30 transform -rotate-3 shadow-2xl flex items-center justify-center">
|
||||||
|
<svg className="w-24 h-24 text-white/80" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M9 12h6m-6 4h6m2 5H7a2 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" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<h3 className="text-2xl font-heading font-bold text-gray-900 dark:text-white">Recent Documents</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DocumentGrid isTrash={false} />
|
<DocumentGrid isTrash={false} />
|
||||||
</UploadArea>
|
</UploadArea>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -33,4 +85,3 @@ export function HomePage() {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@ import { LoginForm } from '@/features/auth/LoginForm'
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] transition-all">
|
<div className="flex min-h-screen items-center justify-center transition-all p-6">
|
||||||
<div className="absolute inset-0 bg-indigo-900/10 dark:bg-black/50 backdrop-blur-[2px]"></div>
|
<div className="relative z-10 w-full flex justify-center">
|
||||||
<div className="relative z-10 w-full flex justify-center px-4">
|
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue