From 60bdc4a3e2ca98313e6bff7aa855ca040f39be98 Mon Sep 17 00:00:00 2001 From: tomymaritano Date: Thu, 23 Apr 2026 20:26:25 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20UX=20polish=20=E2=80=94=20onboarding=20?= =?UTF-8?q?improvements,=20AiSection=20CSS,=20magic=20link=20resend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Onboarding: - Add 4th feature card: "Import Your Notes" (Obsidian, Markdown folders) - Add keyboard shortcut hint: "Press Cmd+K for command palette" - Change "Skip" to "I'll explore on my own" Auth: - Add "Resend magic link" button with 60s cooldown timer - Shows countdown ("Resend in 45s"), spinner while sending AiSection: - Migrate all remaining inline styles to CSS tokens - Map hardcoded rgba colors to --success-muted, --danger-muted, etc. - 15+ new CSS classes in Section.module.css Note: Spellcheck and image paste were already implemented in the codebase. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../renderer/components/Welcome.module.css | 23 ++- .../src/renderer/components/Welcome.tsx | 10 +- .../components/auth/MagicLinkFlow.module.css | 11 ++ .../components/auth/MagicLinkFlow.tsx | 69 ++++++++- .../pages/settings/sections/AiSection.tsx | 124 +++------------ .../settings/sections/Section.module.css | 141 ++++++++++++++++++ 6 files changed, 273 insertions(+), 105 deletions(-) diff --git a/apps/desktop/src/renderer/components/Welcome.module.css b/apps/desktop/src/renderer/components/Welcome.module.css index acb5ca75..255c4698 100644 --- a/apps/desktop/src/renderer/components/Welcome.module.css +++ b/apps/desktop/src/renderer/components/Welcome.module.css @@ -73,6 +73,7 @@ .card:nth-child(1) { animation-delay: 100ms; } .card:nth-child(2) { animation-delay: 200ms; } .card:nth-child(3) { animation-delay: 300ms; } +.card:nth-child(4) { animation-delay: 400ms; } .cardTitle { font-family: var(--font-sans); @@ -96,7 +97,7 @@ align-items: center; gap: var(--space-3); animation: fade-in 400ms ease both; - animation-delay: 400ms; + animation-delay: 500ms; } .skip { @@ -113,3 +114,23 @@ .skip:hover { color: var(--text-secondary); } + +.hint { + font-family: var(--font-sans); + font-size: var(--text-sm); + color: var(--text-faint); + margin: 0; + animation: fade-in 400ms ease both; + animation-delay: 600ms; +} + +.kbd { + display: inline-block; + padding: 1px 6px; + font-family: var(--font-mono, ui-monospace, monospace); + font-size: var(--text-xs); + color: var(--text-secondary); + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: var(--radius-sm); +} diff --git a/apps/desktop/src/renderer/components/Welcome.tsx b/apps/desktop/src/renderer/components/Welcome.tsx index f05d9989..f4de9b04 100644 --- a/apps/desktop/src/renderer/components/Welcome.tsx +++ b/apps/desktop/src/renderer/components/Welcome.tsx @@ -27,6 +27,10 @@ const features = [ title: 'Extensible', desc: 'Plugins, themes, and AI built in.', }, + { + title: 'Import Your Notes', + desc: 'Import from Obsidian, Markdown folders, or other apps.', + }, ] as const; export function Welcome({ onComplete }: WelcomeProps) { @@ -78,9 +82,13 @@ export function Welcome({ onComplete }: WelcomeProps) { Create Your First Note + +

+ Pro tip: Press Cmd+K to open the command palette +

); diff --git a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css index 726c616d..718f29f5 100644 --- a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css +++ b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.module.css @@ -193,3 +193,14 @@ flex-direction: column; gap: 0.75rem; } + +.resendContent { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.spinnerInline { + animation: spin 1s linear infinite; +} diff --git a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx index 267f6ea4..cd0d0854 100644 --- a/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx +++ b/apps/desktop/src/renderer/components/auth/MagicLinkFlow.tsx @@ -4,8 +4,8 @@ * Multi-step dialog for passwordless authentication via email magic link. */ -import { useState, useCallback, useEffect, FormEvent } from 'react'; -import { Mail, CheckCircle, AlertCircle, X } from 'lucide-react'; +import { useState, useCallback, useEffect, useRef, FormEvent } from 'react'; +import { Mail, CheckCircle, AlertCircle, X, RefreshCw } from 'lucide-react'; import { useAuthStore } from '../../stores/authStore'; import styles from './MagicLinkFlow.module.css'; @@ -22,6 +22,9 @@ export function MagicLinkFlow({ onSuccess, onCancel }: MagicLinkFlowProps) { const [email, setEmail] = useState(''); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [resendCooldown, setResendCooldown] = useState(0); + const [isResending, setIsResending] = useState(false); + const cooldownRef = useRef | null>(null); // Watch for auth success (deep link verified in background) useEffect(() => { @@ -64,8 +67,55 @@ export function MagicLinkFlow({ onSuccess, onCancel }: MagicLinkFlowProps) { const handleRetry = useCallback(() => { setStep('email'); setError(null); + setResendCooldown(0); + if (cooldownRef.current) { + clearInterval(cooldownRef.current); + cooldownRef.current = null; + } + }, []); + + const startCooldown = useCallback(() => { + setResendCooldown(60); + if (cooldownRef.current) clearInterval(cooldownRef.current); + cooldownRef.current = setInterval(() => { + setResendCooldown(prev => { + if (prev <= 1) { + if (cooldownRef.current) clearInterval(cooldownRef.current); + cooldownRef.current = null; + return 0; + } + return prev - 1; + }); + }, 1000); }, []); + // Start cooldown when entering "sent" step + useEffect(() => { + if (step === 'sent') { + startCooldown(); + } + return () => { + if (cooldownRef.current) { + clearInterval(cooldownRef.current); + cooldownRef.current = null; + } + }; + }, [step, startCooldown]); + + const handleResend = useCallback(async () => { + if (resendCooldown > 0 || isResending) return; + setIsResending(true); + setError(null); + try { + await requestMagicLink(email); + startCooldown(); + } catch { + setError('Failed to resend magic link. Please try again.'); + } finally { + setIsResending(false); + } + }, [email, requestMagicLink, resendCooldown, isResending, startCooldown]); + return (
e.stopPropagation()}> @@ -122,6 +172,21 @@ export function MagicLinkFlow({ onSuccess, onCancel }: MagicLinkFlowProps) { {error &&

{error}

}
+ diff --git a/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx b/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx index 48f5ed46..ad4b0527 100644 --- a/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx +++ b/apps/desktop/src/renderer/pages/settings/sections/AiSection.tsx @@ -334,31 +334,13 @@ export function AiSection() { {isConnected ? ( /* Connected state */ -
-
-
- +
+
+
+
-
- Connected to {providerInfo.name} -
-
+
Connected to {providerInfo.name}
+
API key stored securely in your system keychain
@@ -375,32 +357,12 @@ export function AiSection() {
) : ( /* Not connected — show connect flow */ -
-
+
+
{currentProvider !== 'ollama' && ( <> -
- +
+ Connect your {providerInfo.name} account
-
+
{ if (e.key === 'Enter') void handleConnect(); }} @@ -452,8 +405,8 @@ export function AiSection() { )} {currentProvider === 'ollama' && ( -
-
+
+
Ollama runs locally — no API key needed. Make sure Ollama is running on your machine.
@@ -470,19 +423,7 @@ export function AiSection() { )} {connectError && ( -
+
{connectError}
@@ -548,7 +489,7 @@ export function AiSection() { {presetMessage?.type === 'success' && (
- + {presetMessage.text} @@ -557,7 +498,7 @@ export function AiSection() { {presetMessage?.type === 'error' && (
- + {presetMessage.text} @@ -565,34 +506,15 @@ export function AiSection() { )} {registeredAiCommands.length > 0 && ( -
-
+
+
Registered AI Commands ({registeredAiCommands.length})
-
+
{registeredAiCommands.map(cmd => ( -
- {cmd.name} - - {cmd.pluginId} - +
+ {cmd.name} + {cmd.pluginId}
))}
diff --git a/apps/desktop/src/renderer/pages/settings/sections/Section.module.css b/apps/desktop/src/renderer/pages/settings/sections/Section.module.css index 3a817652..89f78d1c 100644 --- a/apps/desktop/src/renderer/pages/settings/sections/Section.module.css +++ b/apps/desktop/src/renderer/pages/settings/sections/Section.module.css @@ -905,6 +905,147 @@ background: rgba(239, 68, 68, 0.1); } +/* ============================================================================ + AI Section — Connection States + ============================================================================ */ + +.aiConnectionWrapper { + padding: 1rem 1.25rem; +} + +.aiConnectedBox { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem; + background: var(--success-muted); + border: 1px solid color-mix(in srgb, var(--success) 30%, transparent); + border-radius: 0.75rem; +} + +.aiConnectedInfo { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.aiConnectedIcon { + color: var(--success); +} + +.aiConnectedTitle { + font-weight: 600; + font-size: 0.875rem; + color: var(--text-primary); +} + +.aiConnectedSubtitle { + font-size: 0.75rem; + color: var(--text-muted); + margin-top: 2px; +} + +.aiConnectBox { + padding: 1.25rem; + background: var(--bg-hover); + border: 1px solid var(--border-subtle); + border-radius: 0.75rem; +} + +.aiConnectHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.aiConnectLabel { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary); +} + +.aiKeyInputRow { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.aiKeyInput { + flex: 1; + padding: 0.625rem 0.875rem; + background: var(--bg-base); + border: 1px solid var(--border); + border-radius: 0.5rem; + color: var(--text-primary); + font-size: 0.875rem; + font-family: monospace; +} + +.aiOllamaInfo { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.aiOllamaDescription { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.aiErrorBox { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.5rem; + padding: 0.5rem 0.75rem; + background: var(--danger-muted, rgba(239, 68, 68, 0.08)); + border-radius: 0.5rem; + font-size: 0.8125rem; + color: var(--danger, #ef4444); +} + +.aiCommandListWrapper { + padding: 0.75rem 1rem; +} + +.aiCommandListTitle { + font-size: 0.8125rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.aiCommandList { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.aiCommandItem { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0.75rem; + background: var(--bg-hover); + border-radius: 0.5rem; + font-size: 0.8125rem; +} + +.aiCommandName { + color: var(--text-primary); +} + +.aiCommandPlugin { + color: var(--text-tertiary); + font-size: 0.75rem; +} + +.aiMessageIcon { + display: flex; + align-items: center; + gap: 0.5rem; +} + .deviceActions { padding: 0.5rem 0.75rem 0; }