From b1a411bfdc45ddfb05a0a52deea109055ca791c3 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 12 Apr 2026 08:09:21 +0200 Subject: [PATCH] feat: add global styles and reusable glassmorphism component modules --- .claude/settings.local.json | 7 + app/admin/login/page.tsx | 202 +++++++++++++++++------ app/admin/page.tsx | 202 +++++++++++++++++------ app/globals.css | 207 ++++++++++++++++++------ app/page.module.css | 143 +---------------- app/page.tsx | 171 ++++++++++++++------ components/AddButtonForm.tsx | 180 ++++++++++++++++----- components/AdminButtonRow.tsx | 267 ++++++++++++++++++++++--------- components/EditTitleForm.tsx | 165 +++++++++++++------ components/GlassCard.module.css | 45 +++++- components/MenuButton.module.css | 101 ++++++++++-- components/ThemeToggle.tsx | 35 ++-- 12 files changed, 1188 insertions(+), 537 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..47b3190 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run:*)" + ] + } +} diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx index 52487c4..9f3382b 100644 --- a/app/admin/login/page.tsx +++ b/app/admin/login/page.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { GlassCard } from '@/components/GlassCard'; import { loginAdmin } from '@/app/actions/admin'; -import { Lock } from 'lucide-react'; +import { Lock, AlertCircle, ArrowRight } from 'lucide-react'; export default function AdminLoginPage() { const [error, setError] = useState(null); @@ -24,73 +24,177 @@ export default function AdminLoginPage() { }; return ( -
-
+
+ +
{/* Logo */} -
-
- +
+
+
-

+

Administration

-

+

Accès restreint aux administrateurs

- -
-
-
); diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 24e1a1f..57b13d5 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -22,76 +22,178 @@ export default async function AdminPage() { const appTitle = titleConfig?.value || 'Menu Application'; return ( -
+
+ {/* Header */} -
-
- - +
+
+ + -
-

+

Administration

-

+

{buttons.length} application{buttons.length !== 1 ? 's' : ''} configurée{buttons.length !== 1 ? 's' : ''}

-
+
-
- {/* Edit Title Form */} - +
+ {/* Edit Title Form */} + - {/* Add form */} - + {/* Add form */} + - {/* Button list */} - {buttons.length === 0 ? ( -
- Aucun bouton configuré. -
- ) : ( -
- {(buttons as any[]).map((btn) => ( - - ))} -
- )} + {/* Button list section */} + {buttons.length === 0 ? ( +
+
∅
+

+ Aucune application configurée +

+

+ Commencez par ajouter une application ci-dessus. +

+
+ ) : ( +
+
+ Applications +
+ {(buttons as any[]).map((btn) => ( + + ))} +
+ )} +
); } diff --git a/app/globals.css b/app/globals.css index a25e59d..246d7cf 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,52 +1,112 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); :root { --font: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + + /* Animation Easing */ + --ease-smooth: cubic-bezier(0.25, 0.46, 0.45, 0.94); + --ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-elastic: cubic-bezier(0.4, 0, 0.2, 1); - /* DARK THEME (Obsidian with subtle violet glow) */ - --bg-color: #050505; - --fg: #ffffff; - --fg-secondary: rgba(255, 255, 255, 0.6); - --fg-muted: rgba(255, 255, 255, 0.4); + /* Spacing Scale */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; + --space-2xl: 3rem; - --card-bg: rgba(255, 255, 255, 0.04); + /* DARK THEME (Premium Obsidian) */ + --bg-color: #0a0a0a; + --bg-secondary: #111111; + --fg: #ffffff; + --fg-secondary: rgba(255, 255, 255, 0.65); + --fg-muted: rgba(255, 255, 255, 0.45); + --fg-dim: rgba(255, 255, 255, 0.25); + + --card-bg: rgba(255, 255, 255, 0.05); --card-border: rgba(255, 255, 255, 0.08); --card-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); - --card-hover-bg: rgba(255, 255, 255, 0.08); + --card-hover-bg: rgba(255, 255, 255, 0.1); --card-hover-border: rgba(255, 255, 255, 0.15); --accent-btn-bg: rgba(255, 255, 255, 0.12); - --accent-btn-hover: rgba(255, 255, 255, 0.2); + --accent-btn-hover: rgba(255, 255, 255, 0.18); + + /* Accent Colors */ + --accent-primary: #00d4ff; + --accent-secondary: #7c3aed; + --accent-tertiary: #ec4899; --error: #ff453a; + --error-bg: rgba(255, 69, 58, 0.1); --success: #32d74b; - - --glow-1: rgba(140, 50, 255, 0.15); - --glow-2: rgba(50, 140, 255, 0.15); -} - -[data-theme='light'] { - /* LIGHT THEME (Apple Pearl) */ - --bg-color: #f5f5f7; - --fg: #1d1d1f; - --fg-secondary: rgba(29, 29, 31, 0.65); - --fg-muted: rgba(29, 29, 31, 0.45); - - --card-bg: rgba(255, 255, 255, 0.6); - --card-border: rgba(0, 0, 0, 0.05); - --card-shadow: 0 8px 30px rgba(0, 0, 0, 0.04); - --card-hover-bg: rgba(255, 255, 255, 0.9); - --card-hover-border: rgba(0, 0, 0, 0.1); - - --accent-btn-bg: rgba(0, 0, 0, 0.06); - --accent-btn-hover: rgba(0, 0, 0, 0.12); - - --error: #ff3b30; - --success: #34c759; + --success-bg: rgba(50, 215, 75, 0.1); + --warning: #ff9500; + --warning-bg: rgba(255, 149, 0, 0.1); --glow-1: rgba(140, 50, 255, 0.2); --glow-2: rgba(50, 140, 255, 0.2); + --glow-3: rgba(0, 212, 255, 0.15); +} + +[data-theme='light'] { + /* LIGHT THEME (Modern Clean) */ + --bg-color: #fafbfc; + --bg-secondary: #f0f1f3; + --fg: #0f0f12; + --fg-secondary: rgba(15, 15, 18, 0.7); + --fg-muted: rgba(15, 15, 18, 0.5); + --fg-dim: rgba(15, 15, 18, 0.3); + + --card-bg: rgba(255, 255, 255, 0.7); + --card-border: rgba(0, 0, 0, 0.06); + --card-shadow: 0 8px 30px rgba(0, 0, 0, 0.06); + --card-hover-bg: rgba(255, 255, 255, 0.95); + --card-hover-border: rgba(0, 0, 0, 0.12); + + --accent-btn-bg: rgba(0, 0, 0, 0.07); + --accent-btn-hover: rgba(0, 0, 0, 0.14); + + --accent-primary: #0080cc; + --accent-secondary: #8e44ad; + --accent-tertiary: #e74c8c; + + --error: #ff3b30; + --error-bg: rgba(255, 59, 48, 0.08); + --success: #34c759; + --success-bg: rgba(52, 199, 89, 0.08); + --warning: #ff9500; + --warning-bg: rgba(255, 149, 0, 0.08); + + --glow-1: rgba(140, 50, 255, 0.15); + --glow-2: rgba(50, 140, 255, 0.15); + --glow-3: rgba(0, 128, 204, 0.1); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes slideInLeft { + from { opacity: 0; transform: translateX(-10px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +@keyframes shimmer { + 0% { background-position: -1000px 0; } + 100% { background-position: 1000px 0; } } *, *::before, *::after { @@ -67,11 +127,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow-x: hidden; - transition: background-color 300ms ease, color 300ms ease; + transition: background-color 400ms ease, color 400ms ease; position: relative; } -/* Premium Ambient Glow Orbs */ +/* Premium Ambient Glow Orbs - Enhanced */ body::before, body::after { content: ''; position: fixed; @@ -82,8 +142,8 @@ body::before, body::after { -webkit-filter: blur(180px); z-index: -1; pointer-events: none; - opacity: 0.85; - transition: background 300ms ease; + opacity: 0.6; + transition: background 400ms ease; } body::before { @@ -98,43 +158,86 @@ body::after { background: var(--glow-2); } -a { text-decoration: none; color: inherit; } -button { cursor: pointer; border: none; outline: none; font-family: var(--font); } +a { + text-decoration: none; + color: inherit; + transition: color 250ms var(--ease-smooth); +} -/* Scrollbar */ -::-webkit-scrollbar { width: 8px; } -::-webkit-scrollbar-track { background: transparent; } -::-webkit-scrollbar-thumb { background: var(--card-border); border-radius: 10px; } -::-webkit-scrollbar-thumb:hover { background: var(--fg-muted); } +button { + cursor: pointer; + border: none; + outline: none; + font-family: var(--font); + font-weight: 600; + transition: all 250ms var(--ease-smooth); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Scrollbar - Refined */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--card-border); + border-radius: 10px; + transition: background 250ms ease; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--fg-muted); +} /* Premium Input Styles */ input[type="text"], input[type="password"], input[type="url"], -input[type="email"] { +input[type="email"], +textarea, +select { width: 100%; - padding: 0.85rem 1.1rem; + padding: 0.9rem 1.1rem; background: var(--card-bg); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--card-border); + border: 1.5px solid var(--card-border); border-radius: 12px; color: var(--fg); font-family: var(--font); font-size: 0.95rem; font-weight: 500; outline: none; - transition: all 250ms ease; - box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); + transition: all 280ms var(--ease-smooth); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.02); } -input:focus { - border-color: var(--fg-muted); +input:hover:not(:focus), +textarea:hover:not(:focus), +select:hover:not(:focus) { + border-color: var(--card-hover-border); background: var(--card-hover-bg); - box-shadow: 0 0 0 3px rgba(140, 50, 255, 0.15), inset 0 2px 4px rgba(0,0,0,0.02); } -input::placeholder { +input:focus, +textarea:focus, +select:focus { + border-color: var(--accent-primary); + background: var(--card-hover-bg); + box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1), inset 0 2px 4px rgba(0, 0, 0, 0.02); +} + +input::placeholder, +textarea::placeholder { color: var(--fg-muted); font-weight: 400; } diff --git a/app/page.module.css b/app/page.module.css index 643fe27..726de99 100644 --- a/app/page.module.css +++ b/app/page.module.css @@ -1,142 +1 @@ -.page { - --background: #fafafa; - --foreground: #fff; - - --text-primary: #000; - --text-secondary: #666; - - --button-primary-hover: #383838; - --button-secondary-hover: #f2f2f2; - --button-secondary-border: #ebebeb; - - display: flex; - flex: 1; - flex-direction: column; - align-items: center; - justify-content: center; - font-family: var(--font-geist-sans); - background-color: var(--background); -} - -.main { - display: flex; - flex: 1; - width: 100%; - max-width: 800px; - flex-direction: column; - align-items: flex-start; - justify-content: space-between; - background-color: var(--foreground); - padding: 120px 60px; -} - -.intro { - display: flex; - flex-direction: column; - align-items: flex-start; - text-align: left; - gap: 24px; -} - -.intro h1 { - max-width: 320px; - font-size: 40px; - font-weight: 600; - line-height: 48px; - letter-spacing: -2.4px; - text-wrap: balance; - color: var(--text-primary); -} - -.intro p { - max-width: 440px; - font-size: 18px; - line-height: 32px; - text-wrap: balance; - color: var(--text-secondary); -} - -.intro a { - font-weight: 500; - color: var(--text-primary); -} - -.ctas { - display: flex; - flex-direction: row; - width: 100%; - max-width: 440px; - gap: 16px; - font-size: 14px; -} - -.ctas a { - display: flex; - justify-content: center; - align-items: center; - height: 40px; - padding: 0 16px; - border-radius: 128px; - border: 1px solid transparent; - transition: 0.2s; - cursor: pointer; - width: fit-content; - font-weight: 500; -} - -a.primary { - background: var(--text-primary); - color: var(--background); - gap: 8px; -} - -a.secondary { - border-color: var(--button-secondary-border); -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - a.primary:hover { - background: var(--button-primary-hover); - border-color: transparent; - } - - a.secondary:hover { - background: var(--button-secondary-hover); - border-color: transparent; - } -} - -@media (max-width: 600px) { - .main { - padding: 48px 24px; - } - - .intro { - gap: 16px; - } - - .intro h1 { - font-size: 32px; - line-height: 40px; - letter-spacing: -1.92px; - } -} - -@media (prefers-color-scheme: dark) { - .logo { - filter: invert(); - } - - .page { - --background: #000; - --foreground: #000; - - --text-primary: #ededed; - --text-secondary: #999; - - --button-primary-hover: #ccc; - --button-secondary-hover: #1a1a1a; - --button-secondary-border: #1a1a1a; - } -} +/* Legacy styles - not in use. See globals.css and inline styles in app/page.tsx */ diff --git a/app/page.tsx b/app/page.tsx index 3d40a6d..add14ec 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -21,34 +21,90 @@ export default async function Home() { return (
+ {/* Top controls */} -
- - - - - +
+
+ + + + +
{/* Center content */} @@ -58,56 +114,73 @@ export default async function Home() { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - padding: '2rem', - gap: '3rem', + padding: 'max(2rem, 4vw)', + gap: '2.5rem', }}> {/* Title */} -
-

+

{appTitle}

{buttons.length > 0 && ( -

+

{buttons.length} application{buttons.length > 1 ? 's' : ''} disponible{buttons.length > 1 ? 's' : ''}

)}
- {/* Grid */} + {/* Grid or Empty State */} {buttons.length === 0 ? ( -
-
🔗
-

+

+
🔗
+

Aucune application

-

+

Ajoutez vos raccourcis depuis l'administration.

- + Configurer →
) : ( -
{(buttons as any[]).map((btn) => ( diff --git a/components/AddButtonForm.tsx b/components/AddButtonForm.tsx index aa692a9..63c0e62 100644 --- a/components/AddButtonForm.tsx +++ b/components/AddButtonForm.tsx @@ -4,16 +4,18 @@ import React, { useState } from 'react'; import { addButton } from '@/app/actions/admin'; import { IconPicker } from '@/components/IconPicker'; import { serializeIcon } from '@/lib/lucideIcons'; -import { PlusCircle } from 'lucide-react'; +import { PlusCircle, Check } from 'lucide-react'; export function AddButtonForm() { const [iconName, setIconName] = useState('Globe'); const [iconColor, setIconColor] = useState('#22d3ee'); const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); + setSuccess(false); const formData = new FormData(e.currentTarget); formData.set('icon', serializeIcon(iconName, iconColor)); try { @@ -21,6 +23,8 @@ export function AddButtonForm() { (e.target as HTMLFormElement).reset(); setIconName('Globe'); setIconColor('#22d3ee'); + setSuccess(true); + setTimeout(() => setSuccess(false), 2000); } finally { setLoading(false); } @@ -33,29 +37,90 @@ export function AddButtonForm() { background: 'var(--card-bg)', backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)', - border: '1px solid var(--card-border)', + border: '1.5px solid var(--card-border)', borderRadius: '20px', - padding: '1.75rem', - marginBottom: '2rem', + padding: 'clamp(1.5rem, 3vw, 2rem)', + marginBottom: 'clamp(1.5rem, 3vw, 2.5rem)', + transition: 'all 300ms ease', + animation: 'fadeInUp 500ms cubic-bezier(0.34, 1.56, 0.64, 1) 150ms both', }}> + +

- AJOUTER UNE APPLICATION + Ajouter une application

-
+ {/* Icon picker */} -
-