From 6cdf40b44eb1dc1c4f9fae14a0c09ee5afff4979 Mon Sep 17 00:00:00 2001 From: decolua Date: Sun, 3 May 2026 18:00:35 +0700 Subject: [PATCH] Refactor global styles and enhance MITM functionality - Updated global CSS to implement a new brand color palette and improve light/dark theme consistency. - Enhanced the MitmServerCard component to provide clearer user feedback regarding admin privileges. - Filtered LLM combos in the CombosPage to ensure only relevant data is displayed. - Improved APIPageClient layout for better usability and visual consistency. - Added functionality to save and load DNS tool states in the MITM manager. - Updated OAuth configuration URLs for Qwen to reflect the new endpoint structure. - Refined tunnel management logic to improve reliability and user experience. --- .../cli-tools/components/MitmServerCard.js | 3 + src/app/(dashboard)/dashboard/combos/page.js | 3 +- .../dashboard/endpoint/EndpointPageClient.js | 97 ++-- .../dashboard/mitm/MitmPageClient.js | 6 +- .../dashboard/providers/[id]/page.js | 25 +- src/app/(dashboard)/dashboard/usage/page.js | 42 +- .../api/cli-tools/antigravity-mitm/route.js | 34 +- src/app/api/v1/models/route.js | 12 - src/app/api/v1/route.js | 33 +- src/app/globals.css | 444 ++++++++++++------ src/app/login/page.js | 6 +- src/lib/appUpdater.js | 8 +- src/lib/localDb.js | 1 + src/lib/oauth/constants/oauth.js | 4 +- src/lib/tunnel/cloudflared.js | 6 +- src/lib/tunnel/networkProbe.js | 67 +++ src/lib/tunnel/tunnelConfig.js | 18 + src/lib/tunnel/tunnelManager.js | 219 ++++----- src/mitm/dns/dnsConfig.js | 56 ++- src/mitm/manager.js | 64 ++- src/mitm/winElevated.js | 14 +- src/shared/components/Badge.js | 7 +- src/shared/components/Button.js | 22 +- src/shared/components/Card.js | 36 +- src/shared/components/Drawer.js | 45 +- src/shared/components/Header.js | 22 +- src/shared/components/Input.js | 14 +- src/shared/components/Loading.js | 10 +- src/shared/components/Modal.js | 45 +- src/shared/components/NineRemotePromoModal.js | 28 +- src/shared/components/OAuthModal.js | 4 + src/shared/components/SegmentedControl.js | 8 +- src/shared/components/Select.js | 13 +- src/shared/components/Sidebar.js | 42 +- src/shared/components/ThemeToggle.js | 17 +- src/shared/components/Toggle.js | 37 +- src/shared/components/UsageStats.js | 44 +- .../components/layouts/DashboardLayout.js | 4 +- src/shared/constants/providers.js | 22 +- src/shared/services/initializeApp.js | 209 +++++---- 40 files changed, 1029 insertions(+), 762 deletions(-) create mode 100644 src/lib/tunnel/networkProbe.js create mode 100644 src/lib/tunnel/tunnelConfig.js diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js index c171ad12..27808963 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js @@ -23,6 +23,8 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } const serverIsWindows = status?.isWin === true; const canRunWithoutPassword = serverIsWindows || status?.hasCachedPassword || status?.needsSudoPassword === false; const isAdmin = status?.isAdmin !== false; + // No privilege: not admin/root AND (Win OR no cached sudo password) + const noPrivilege = !isAdmin && (serverIsWindows || (!status?.hasCachedPassword && status?.needsSudoPassword !== false)); const fetchStatus = useCallback(async () => { try { @@ -217,6 +219,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } @@ -750,8 +749,8 @@ export default function APIPageClient({ machineId }) { {/* Tailscale */}
- Tailscale {tsEnabled && !tsLoading ? ( <> @@ -850,8 +849,8 @@ export default function APIPageClient({ machineId }) {

Token Saver

-
-
+
+ handleRtkEnabled(!rtkEnabled)} />
-
-
+ - {cavemanEnabled && ( -
- {CAVEMAN_LEVELS.map((lvl) => ( - - ))} +
+ {cavemanEnabled && ( +
+ {CAVEMAN_LEVELS.map((lvl) => ( + + ))} +
+ )} + handleCavemanEnabled(!cavemanEnabled)} + />
- )} +
{/* API Keys */} @@ -1089,14 +1090,14 @@ export default function APIPageClient({ machineId }) { onClose={() => setShowEnableTunnelModal(false)} >
-
+
- cloud_upload + cloud_upload
-

+

Cloudflare Tunnel

-

+

Expose your local 9Router to the internet. No port forwarding, no static IP needed. Share endpoint URL with your team or use it in Cursor, Cline, and other AI tools from anywhere.

@@ -1118,11 +1119,7 @@ export default function APIPageClient({ machineId }) {

- @@ -1139,7 +1136,7 @@ export default function APIPageClient({ machineId }) {

The Cloudflare tunnel will be disconnected. Remote access via tunnel URL will stop working.

- @@ -1167,11 +1164,7 @@ export default function APIPageClient({ machineId }) {

Tailscale is not installed. Install it to enable Funnel.

- @@ -1211,7 +1204,6 @@ export default function APIPageClient({ machineId }) { handleConnectTailscale(tab); }} fullWidth - className="bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!" > Connect @@ -1233,7 +1225,7 @@ export default function APIPageClient({ machineId }) {

Tailscale Funnel will be stopped. Remote access via Tailscale URL will stop working.

- @@ -1248,9 +1240,8 @@ export default function APIPageClient({ machineId }) { function EndpointRow({ label, url, copyId, copied, onCopy, badge, actions }) { return (
- {label} )} -
diff --git a/src/app/(dashboard)/dashboard/usage/page.js b/src/app/(dashboard)/dashboard/usage/page.js index fc95c04d..52a344d6 100644 --- a/src/app/(dashboard)/dashboard/usage/page.js +++ b/src/app/(dashboard)/dashboard/usage/page.js @@ -5,6 +5,13 @@ import { useSearchParams, useRouter } from "next/navigation"; import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components"; import RequestDetailsTab from "./components/RequestDetailsTab"; +const PERIODS = [ + { value: "24h", label: "24h" }, + { value: "7d", label: "7D" }, + { value: "30d", label: "30D" }, + { value: "60d", label: "60D" }, +]; + export default function UsagePage() { return ( }> @@ -18,6 +25,7 @@ function UsageContent() { const router = useRouter(); const [tabLoading, setTabLoading] = useState(false); + const [period, setPeriod] = useState("7d"); const tabFromUrl = searchParams.get("tab"); const activeTab = tabFromUrl && ["overview", "logs", "details"].includes(tabFromUrl) @@ -30,21 +38,32 @@ function UsageContent() { const params = new URLSearchParams(searchParams); params.set("tab", value); router.push(`/dashboard/usage?${params.toString()}`, { scroll: false }); - // Brief loading flash so user sees feedback setTimeout(() => setTabLoading(false), 300); }; return (
- + {/* Tabs + period selector on same row */} +
+ + {activeTab === "overview" && ( + + )} +
{tabLoading ? ( @@ -52,7 +71,7 @@ function UsageContent() { <> {activeTab === "overview" && ( }> - + )} {activeTab === "logs" && } @@ -62,4 +81,3 @@ function UsageContent() {
); } - diff --git a/src/app/api/cli-tools/antigravity-mitm/route.js b/src/app/api/cli-tools/antigravity-mitm/route.js index 9852f947..658c314c 100644 --- a/src/app/api/cli-tools/antigravity-mitm/route.js +++ b/src/app/api/cli-tools/antigravity-mitm/route.js @@ -46,13 +46,22 @@ function requiresSudoPassword(pwd) { } function checkIsAdmin() { - if (!isWin) return true; - try { - require("child_process").execSync("net session >nul 2>&1", { windowsHide: true }); - return true; - } catch { - return false; + if (isWin) { + try { + require("child_process").execSync("net session >nul 2>&1", { windowsHide: true }); + return true; + } catch { + return false; + } } + return typeof process.getuid === "function" && process.getuid() === 0; +} + +function checkPrivilege(pwd) { + if (checkIsAdmin()) return true; + if (isWin) return false; + if (!isSudoPasswordRequired()) return true; + return !!pwd; } // GET - Full MITM status (server + per-tool DNS) @@ -94,6 +103,13 @@ export async function POST(request) { ); } + if (!checkPrivilege(pwd)) { + return NextResponse.json( + { error: isWin ? "Administrator required — restart 9Router as Administrator" : "Root or sudo password required to start MITM" }, + { status: 403 } + ); + } + if (mitmRouterBaseUrl !== undefined && mitmRouterBaseUrl !== null) { try { const normalized = normalizeMitmRouterBaseUrlInput(mitmRouterBaseUrl); @@ -149,6 +165,12 @@ export async function PATCH(request) { if (requiresSudoPassword(pwd)) { return NextResponse.json({ error: "Missing sudoPassword" }, { status: 400 }); } + if (!checkPrivilege(pwd)) { + return NextResponse.json( + { error: isWin ? "Administrator required — restart 9Router as Administrator" : "Root or sudo password required to modify DNS" }, + { status: 403 } + ); + } if (action === "enable") { await enableToolDNS(tool, pwd); diff --git a/src/app/api/v1/models/route.js b/src/app/api/v1/models/route.js index cfbe56e3..7003c602 100644 --- a/src/app/api/v1/models/route.js +++ b/src/app/api/v1/models/route.js @@ -138,9 +138,6 @@ export async function GET() { object: "model", created: timestamp, owned_by: "combo", - permission: [], - root: combo.name, - parent: null, }); } @@ -154,9 +151,6 @@ export async function GET() { object: "model", created: timestamp, owned_by: alias, - permission: [], - root: model.id, - parent: null, }); } } @@ -175,9 +169,6 @@ export async function GET() { object: "model", created: timestamp, owned_by: providerAlias, - permission: [], - root: modelId, - parent: null, }); } } else { @@ -267,9 +258,6 @@ export async function GET() { object: "model", created: timestamp, owned_by: outputAlias, - permission: [], - root: modelId, - parent: null, }); } } diff --git a/src/app/api/v1/route.js b/src/app/api/v1/route.js index 382c6c3d..7f44b6db 100644 --- a/src/app/api/v1/route.js +++ b/src/app/api/v1/route.js @@ -1,32 +1 @@ -const CORS_HEADERS = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, OPTIONS", - "Access-Control-Allow-Headers": "*" -}; - -/** - * Handle CORS preflight - */ -export async function OPTIONS() { - return new Response(null, { headers: CORS_HEADERS }); -} - -/** - * GET /v1 - Return models list (OpenAI compatible) - */ -export async function GET() { - const models = [ - { id: "claude-sonnet-4-20250514", object: "model", owned_by: "anthropic" }, - { id: "claude-3-5-sonnet-20241022", object: "model", owned_by: "anthropic" }, - { id: "gpt-4o", object: "model", owned_by: "openai" }, - { id: "gemini-2.5-pro", object: "model", owned_by: "google" } - ]; - - return new Response(JSON.stringify({ - object: "list", - data: models - }), { - headers: { "Content-Type": "application/json", ...CORS_HEADERS } - }); -} - +export { GET, OPTIONS } from "./models/route"; diff --git a/src/app/globals.css b/src/app/globals.css index 445c0b87..4decd9e1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,80 +3,183 @@ @custom-variant dark (&:where(.dark, .dark *)); -/* macOS-inspired Color Palette with Terracotta Primary */ +/* ============================================================ + 9Router palette — adopted from 9remote_private/web + Brand orange (dark) / soft coral (light), neutral warm bases + ============================================================ */ :root { - /* Primary - Warm Coral/Terracotta */ - --color-primary: #D97757; - --color-primary-hover: #C56243; - - /* Light theme */ - --color-bg: #FBF9F6; - --color-bg-alt: #F5F1ED; - --color-surface: #FFFFFF; - --color-sidebar: rgba(246, 246, 246, 0.8); - --color-border: rgba(0, 0, 0, 0.1); - --color-text-main: #383733; - --color-text-muted: #75736E; - - /* Shadows - subtle macOS style */ - --shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.02), 0 4px 12px rgba(0, 0, 0, 0.015); - --shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.12); + /* Brand scale (light) - centered on #E56A4A */ + --color-brand-50: #fdf1ed; + --color-brand-100: #fadccf; + --color-brand-200: #f4b59c; + --color-brand-300: #ee8d6a; + --color-brand-400: #ea7855; + --color-brand-500: #E56A4A; + --color-brand-600: #cc5236; + --color-brand-700: #a64027; + --color-brand-800: #7a2f1d; + --color-brand-900: #4d1e12; + + /* Primary (legacy alias for backward compat with existing components) */ + --color-primary: var(--color-brand-500); + --color-primary-hover: var(--color-brand-600); + + /* Surfaces & backgrounds (light) */ + --color-bg: #FDFAF6; + --color-bg-alt: #F7F3EE; + --color-surface: #ffffff; + --color-surface-2: #f4f4f5; + --color-surface-3: #e7e7e9; + --color-sidebar: rgba(244, 241, 236, 0.85); + + /* Borders */ + --color-border: #e5e7eb; + --color-border-subtle: #f1f1f3; + + /* Text */ + --color-text: #0a0a0a; + --color-text-main: #0a0a0a; + --color-text-muted: #6B7280; + --color-text-subtle: #9CA3AF; + + /* Status */ + --color-danger: #cf222e; + --color-success: #10B981; + --color-warning: #F59E0B; + --color-info: #3B82F6; + + /* Radius */ + --radius-brand: 10px; + --radius-brand-lg: 14px; + + /* Shadows */ + --shadow-soft: 0 1px 2px 0 rgba(0,0,0,0.04); + --shadow-warm: 0 2px 12px -2px rgba(229, 106, 74, 0.18); --shadow-elevated: 0 12px 28px -4px rgba(60, 50, 45, 0.06); + --shadow-elev: + inset 0 1px 0 0 rgba(255,255,255,0.8), + 0 1px 2px rgba(15,23,42,0.04), + 0 12px 36px -8px rgba(15,23,42,0.10); + --shadow-focus: 0 0 0 3px rgba(229,106,74,0.18); + + color-scheme: light; } .dark { - /* Dark theme */ - --color-bg: #191918; + /* Brand scale (dark) - centered on #E56A4A, same as light for consistency */ + --color-brand-50: #fdf1ed; + --color-brand-100: #fadccf; + --color-brand-200: #f4b59c; + --color-brand-300: #ee8d6a; + --color-brand-400: #ea7855; + --color-brand-500: #E56A4A; + --color-brand-600: #cc5236; + --color-brand-700: #a64027; + --color-brand-800: #7a2f1d; + --color-brand-900: #4d1e12; + + --color-primary: #E56A4A; + --color-primary-hover: #cc5236; + + /* Surfaces (dark - Claude-like neutral warm) */ + --color-bg: #1a1a1a; --color-bg-alt: #1F1F1E; - --color-surface: #242423; - --color-sidebar: rgba(30, 30, 30, 0.8); - --color-border: rgba(255, 255, 255, 0.1); - --color-text-main: #ECEBE8; - --color-text-muted: #9E9D99; - - /* Dark shadows - subtle macOS style */ - --shadow-soft: 0 1px 3px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1); - --shadow-warm: 0 2px 12px -2px rgba(217, 119, 87, 0.15); - --shadow-elevated: 0 12px 28px -4px rgba(0, 0, 0, 0.3); + --color-surface: #262626; + --color-surface-2: #303030; + --color-surface-3: #3a3a3a; + --color-sidebar: rgba(30, 30, 30, 0.85); + + --color-border: #333333; + --color-border-subtle: #2a2a2a; + + --color-text: #ededed; + --color-text-main: #ededed; + --color-text-muted: #9ca3af; + --color-text-subtle: #6b7280; + + --color-danger: #ef4444; + --color-success: #22c55e; + --color-warning: #fbbf24; + --color-info: #60a5fa; + + --shadow-soft: 0 1px 2px 0 rgba(0,0,0,0.3); + --shadow-warm: 0 2px 12px -2px rgba(229, 106, 74, 0.25); + --shadow-elevated: 0 12px 28px -4px rgba(0, 0, 0, 0.45); + --shadow-elev: + inset 0 1px 0 0 rgba(255,255,255,0.06), + 0 1px 2px rgba(0,0,0,0.4), + 0 16px 48px -8px rgba(0,0,0,0.55); + --shadow-focus: 0 0 0 3px rgba(229, 106, 74, 0.18); + + color-scheme: dark; } @theme inline { - /* Primary */ + /* Brand scale */ + --color-brand-50: var(--color-brand-50); + --color-brand-100: var(--color-brand-100); + --color-brand-200: var(--color-brand-200); + --color-brand-300: var(--color-brand-300); + --color-brand-400: var(--color-brand-400); + --color-brand-500: var(--color-brand-500); + --color-brand-600: var(--color-brand-600); + --color-brand-700: var(--color-brand-700); + --color-brand-800: var(--color-brand-800); + --color-brand-900: var(--color-brand-900); + + /* Primary aliases */ --color-primary: var(--color-primary); --color-primary-hover: var(--color-primary-hover); - - /* Auto-switch colors (use CSS variables from :root/.dark) */ + + /* Semantic */ --color-bg: var(--color-bg); + --color-bg-alt: var(--color-bg-alt); --color-surface: var(--color-surface); + --color-surface-2: var(--color-surface-2); + --color-surface-3: var(--color-surface-3); --color-sidebar: var(--color-sidebar); --color-border: var(--color-border); + --color-border-subtle: var(--color-border-subtle); + --color-text: var(--color-text); --color-text-main: var(--color-text-main); --color-text-muted: var(--color-text-muted); - - /* Static colors (for explicit light/dark usage) */ - --color-bg-light: #FBF9F6; - --color-bg-dark: #191918; - --color-surface-light: #FFFFFF; - --color-surface-dark: #242423; - --color-sidebar-light: #F0EFEC; + --color-text-subtle: var(--color-text-subtle); + --color-accent: var(--color-brand-500); + --color-danger: var(--color-danger); + --color-success: var(--color-success); + --color-warning: var(--color-warning); + --color-info: var(--color-info); + + /* Static fallbacks (explicit per-mode usage if needed) */ + --color-bg-light: #FCFBF9; + --color-bg-dark: #1a1a1a; + --color-surface-light: #ffffff; + --color-surface-dark: #262626; + --color-sidebar-light: #F4F1EC; --color-sidebar-dark: #1F1F1E; - --color-border-light: #E6E4DD; - --color-border-dark: #333331; - --color-text-main-light: #383733; - --color-text-main-dark: #ECEBE8; - --color-text-muted-light: #75736E; - --color-text-muted-dark: #9E9D99; - + --color-border-light: #e5e7eb; + --color-border-dark: #333333; + --color-text-main-light: #0a0a0a; + --color-text-main-dark: #ededed; + --color-text-muted-light: #6B7280; + --color-text-muted-dark: #9ca3af; + + /* Radius */ + --radius-brand: var(--radius-brand); + --radius-brand-lg: var(--radius-brand-lg); + /* Shadows */ --shadow-soft: var(--shadow-soft); --shadow-warm: var(--shadow-warm); --shadow-elevated: var(--shadow-elevated); - - /* Font - macOS system fonts */ - --font-sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', system-ui, sans-serif; + --shadow-elev: var(--shadow-elev); + --shadow-focus: var(--shadow-focus); + + /* Font - Inter primary, Apple system fallback */ + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', system-ui, sans-serif; } -/* Base styles */ +/* Base */ body { background-color: var(--color-bg); color: var(--color-text-main); @@ -85,38 +188,94 @@ body { -moz-osx-font-smoothing: grayscale; } -/* Selection */ +/* Selection - brand-tinted */ ::selection { - background-color: rgba(217, 119, 87, 0.2); + background-color: rgba(229, 106, 74, 0.25); color: var(--color-primary); } +.dark ::selection { + background-color: rgba(229, 106, 74, 0.3); + color: var(--color-brand-300); +} -/* Custom scrollbar */ +/* iOS keyboard accent */ +input, textarea { + accent-color: var(--color-primary); +} + +/* Scrollbar (custom-scrollbar utility kept for compat) */ .custom-scrollbar::-webkit-scrollbar { width: 6px; } - .custom-scrollbar::-webkit-scrollbar-track { background: transparent; } - .custom-scrollbar::-webkit-scrollbar-thumb { background-color: rgba(156, 163, 175, 0.3); border-radius: 20px; } - .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background-color: rgba(156, 163, 175, 0.5); + background-color: var(--color-primary); } -/* Hero gradient */ +/* Thin horizontal scrollbar - brand colored */ +.scroll-thin-x { + scrollbar-width: thin; + scrollbar-color: rgba(229, 106, 74, 0.55) transparent; +} +.dark .scroll-thin-x { + scrollbar-color: rgba(229, 106, 74, 0.55) transparent; +} +.scroll-thin-x::-webkit-scrollbar { height: 3px; } +.scroll-thin-x::-webkit-scrollbar-track { background: transparent; } +.scroll-thin-x::-webkit-scrollbar-thumb { + background: rgba(229, 106, 74, 0.55); + border-radius: 3px; +} +.dark .scroll-thin-x::-webkit-scrollbar-thumb { + background: rgba(229, 106, 74, 0.55); +} + +/* Reusable elevated card */ +.card-soft { + background-color: var(--color-surface); + box-shadow: var(--shadow-soft); + border-radius: var(--radius-brand-lg); +} +.card-elev { + background-color: var(--color-surface); + box-shadow: var(--shadow-elev); + border-radius: var(--radius-brand-lg); +} + +/* Hero gradient (compat) */ .bg-hero-gradient { - background: linear-gradient(180deg, #F5F1ED 0%, #FEFCFB 100%); + background: linear-gradient(180deg, var(--color-bg-alt) 0%, var(--color-bg) 100%); } -.dark .bg-hero-gradient { - background: linear-gradient(180deg, #1F1F1E 0%, #191918 100%); +/* macOS Vibrancy */ +.bg-vibrancy { + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + background: rgba(255, 255, 255, 0.72); } +.dark .bg-vibrancy { + background: rgba(38, 38, 38, 0.72); +} + +/* macOS Traffic Lights */ +.traffic-lights { + display: flex; + gap: 8px; +} +.traffic-light { + width: 12px; + height: 12px; + border-radius: 50%; +} +.traffic-light.red { background: #FF5F56; } +.traffic-light.yellow { background: #FFBD2E; } +.traffic-light.green { background: #27C93F; } /* Material Symbols */ .material-symbols-outlined { @@ -136,86 +295,117 @@ body { -webkit-font-smoothing: antialiased; font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; } - .material-symbols-outlined.fill-1 { font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 24; } -/* Animations */ -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +/* Disable text selection on buttons */ +button { + -webkit-user-select: none; + user-select: none; } -.animate-spin { - animation: spin 1s linear infinite; +/* ============================================================ + Animations + ============================================================ */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } +.animate-spin { animation: spin 1s linear infinite; } @keyframes pulse { - 0%, 100% { - opacity: 1; - } - 50% { - opacity: 0.5; - } -} - -.animate-pulse { - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } } +.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } @keyframes border-glow { 0%, 100% { - box-shadow: 0 0 5px rgba(217, 119, 87, 0.3), 0 0 10px rgba(217, 119, 87, 0.2); - border-color: rgba(217, 119, 87, 0.5); + box-shadow: 0 0 5px rgba(229, 106, 74, 0.3), 0 0 10px rgba(229, 106, 74, 0.2); + border-color: rgba(229, 106, 74, 0.5); } 50% { - box-shadow: 0 0 10px rgba(217, 119, 87, 0.5), 0 0 20px rgba(217, 119, 87, 0.3); - border-color: rgba(217, 119, 87, 0.8); + box-shadow: 0 0 10px rgba(229, 106, 74, 0.5), 0 0 20px rgba(229, 106, 74, 0.3); + border-color: rgba(229, 106, 74, 0.8); } } +.animate-border-glow { animation: border-glow 2s ease-in-out infinite; } -.animate-border-glow { - animation: border-glow 2s ease-in-out infinite; +@keyframes slideInFromRight { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes slideInFromTop { + from { transform: translateY(-10px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} +.slide-in-right { animation: slideInFromRight 0.25s cubic-bezier(0.22, 1, 0.36, 1) forwards; } +.fade-in { animation: fadeIn 0.2s ease-out forwards; } +.slide-in-top { animation: slideInFromTop 0.18s cubic-bezier(0.22, 1, 0.36, 1) forwards; } + +@keyframes pulseGlow { + 0%, 100% { opacity: 0.4; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(1.05); } +} +.animate-pulse-glow { animation: pulseGlow 3s ease-in-out infinite; } + +/* CTA shimmer + glow pulse */ +@keyframes ctaShimmer { + 0% { transform: translateX(-120%) skewX(-20deg); } + 100% { transform: translateX(220%) skewX(-20deg); } +} +@keyframes ctaGlowPulse { + 0%, 100% { box-shadow: 0 8px 24px -8px rgba(229, 106, 74, 0.45), 0 0 0 0 rgba(229, 106, 74, 0.5); } + 50% { box-shadow: 0 12px 32px -8px rgba(229, 106, 74, 0.6), 0 0 0 8px rgba(229, 106, 74, 0); } +} +.btn-cta { + position: relative; + overflow: hidden; + isolation: isolate; + animation: ctaGlowPulse 2.4s ease-in-out infinite; +} +.btn-cta::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.35), transparent); + animation: ctaShimmer 2.8s ease-in-out infinite; + pointer-events: none; + z-index: 1; +} +.btn-cta > * { position: relative; z-index: 2; } + +/* Dot Grid Background Pattern */ +.dot-grid-bg { + background-color: var(--color-bg); + background-image: + radial-gradient(circle at 15% 20%, rgba(229, 106, 74, 0.10) 0%, transparent 40%), + radial-gradient(circle at 85% 80%, rgba(229, 106, 74, 0.06) 0%, transparent 40%); +} +.dark .dot-grid-bg { + background-image: + radial-gradient(circle at 15% 20%, rgba(229, 106, 74, 0.18) 0%, transparent 40%), + radial-gradient(circle at 85% 80%, rgba(229, 106, 74, 0.10) 0%, transparent 40%); } -/* macOS Vibrancy/Blur Effect */ -.bg-vibrancy { - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - background: rgba(255, 255, 255, 0.72); +/* Landing-style faint grid overlay (use absolute pos inside relative parent) */ +.landing-grid { + background-image: + linear-gradient(to right, var(--color-accent) 1px, transparent 1px), + linear-gradient(to bottom, var(--color-accent) 1px, transparent 1px); + background-size: 40px 40px; + opacity: 0.08; } - -.dark .bg-vibrancy { - background: rgba(30, 30, 30, 0.72); -} - -/* macOS Traffic Lights */ -.traffic-lights { - display: flex; - gap: 8px; -} - -.traffic-light { - width: 12px; - height: 12px; - border-radius: 50%; -} - -.traffic-light.red { - background: #FF5F56; -} - -.traffic-light.yellow { - background: #FFBD2E; -} - -.traffic-light.green { - background: #27C93F; +.dark .landing-grid { + opacity: 0.04; } /* Changelog markdown body */ @@ -226,9 +416,7 @@ body { padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-border); } -.changelog-body h1:first-child { - margin-top: 0; -} +.changelog-body h1:first-child { margin-top: 0; } .changelog-body h2 { font-size: 1.05rem; font-weight: 600; @@ -245,14 +433,8 @@ body { padding-left: 1.5rem; margin: 0.5rem 0; } -.changelog-body li { - margin: 0.25rem 0; - line-height: 1.6; -} -.changelog-body p { - margin: 0.5rem 0; - line-height: 1.6; -} +.changelog-body li { margin: 0.25rem 0; line-height: 1.6; } +.changelog-body p { margin: 0.5rem 0; line-height: 1.6; } .changelog-body code { background: var(--color-bg-alt); padding: 0.1rem 0.35rem; diff --git a/src/app/login/page.js b/src/app/login/page.js index bb87b848..679dd4c2 100644 --- a/src/app/login/page.js +++ b/src/app/login/page.js @@ -82,8 +82,10 @@ export default function LoginPage() { } return ( -
-
+
+ {/* Faint grid background */} +