import { useState, useRef, useCallback, useEffect } from 'react' import { X, GithubLogo, SignOut } from '@phosphor-icons/react' import { GitHubDeviceFlow } from './GitHubDeviceFlow' import type { Settings } from '../types' import { trackEvent } from '../lib/telemetry' import { Switch } from './ui/switch' interface SettingsPanelProps { open: boolean settings: Settings onSave: (settings: Settings) => void explicitOrganizationEnabled?: boolean onSaveExplicitOrganization?: (enabled: boolean) => void onClose: () => void } // --- GitHub OAuth Section --- interface GitHubSectionProps { githubUsername: string | null githubToken: string | null onConnected: (token: string, username: string) => void onDisconnect: () => void } function GitHubSection({ githubUsername, githubToken, onConnected, onDisconnect }: GitHubSectionProps) { const isConnected = !!githubToken && !!githubUsername if (isConnected) { return } return } function GitHubConnectedRow({ username, onDisconnect }: { username: string; onDisconnect: () => void }) { return (
{username} Connected
) } // --- Settings Panel --- function isSaveShortcut(event: React.KeyboardEvent): boolean { return event.key === 'Enter' && (event.metaKey || event.ctrlKey) } export function SettingsPanel({ open, settings, onSave, explicitOrganizationEnabled = true, onSaveExplicitOrganization, onClose, }: SettingsPanelProps) { if (!open) return null return ( ) } type SettingsPanelInnerProps = Omit & { explicitOrganizationEnabled: boolean } function SettingsPanelInner({ settings, onSave, explicitOrganizationEnabled, onSaveExplicitOrganization, onClose, }: SettingsPanelInnerProps) { const [githubToken, setGithubToken] = useState(settings.github_token) const [githubUsername, setGithubUsername] = useState(settings.github_username) const [pullInterval, setPullInterval] = useState(settings.auto_pull_interval_minutes ?? 5) const [releaseChannel, setReleaseChannel] = useState(settings.release_channel ?? 'stable') const [crashReporting, setCrashReporting] = useState(settings.crash_reporting_enabled ?? false) const [analytics, setAnalytics] = useState(settings.analytics_enabled ?? false) const [explicitOrganization, setExplicitOrganization] = useState(explicitOrganizationEnabled) const panelRef = useRef(null) // Auto-focus first input when settings panel opens useEffect(() => { const timer = setTimeout(() => { const input = panelRef.current?.querySelector('input') input?.focus() }, 50) return () => clearTimeout(timer) }, []) const buildSettings = useCallback((ghOverride?: { token: string | null; username: string | null }): Settings => ({ github_token: ghOverride ? ghOverride.token : (githubToken ?? null), github_username: ghOverride ? ghOverride.username : (githubUsername ?? null), auto_pull_interval_minutes: pullInterval, telemetry_consent: (crashReporting || analytics) ? true : (settings.telemetry_consent === null ? null : false), crash_reporting_enabled: crashReporting, analytics_enabled: analytics, anonymous_id: (crashReporting || analytics) ? (settings.anonymous_id ?? crypto.randomUUID()) : settings.anonymous_id, release_channel: releaseChannel === 'stable' ? null : releaseChannel, }), [githubToken, githubUsername, pullInterval, releaseChannel, crashReporting, analytics, settings.telemetry_consent, settings.anonymous_id]) const handleSave = () => { const prevAnalytics = settings.analytics_enabled ?? false const newAnalytics = analytics if (!prevAnalytics && newAnalytics) trackEvent('telemetry_opted_in') if (prevAnalytics && !newAnalytics) trackEvent('telemetry_opted_out') onSave(buildSettings()) onSaveExplicitOrganization?.(explicitOrganization) onClose() } const handleGitHubConnected = useCallback((token: string, username: string) => { setGithubToken(token) setGithubUsername(username) onSave(buildSettings({ token, username })) }, [onSave, buildSettings]) const handleGitHubDisconnect = useCallback(() => { setGithubToken(null) setGithubUsername(null) onSave(buildSettings({ token: null, username: null })) }, [onSave, buildSettings]) const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.stopPropagation() onClose() } if (isSaveShortcut(e)) { e.preventDefault() handleSave() } } return (
{ if (e.target === e.currentTarget) onClose() }} onKeyDown={handleKeyDown} data-testid="settings-panel" >
) } function SettingsHeader({ onClose }: { onClose: () => void }) { return (
Settings
) } interface SettingsBodyProps { githubToken: string | null; githubUsername: string | null onGitHubConnected: (token: string, username: string) => void onGitHubDisconnect: () => void pullInterval: number; setPullInterval: (v: number) => void releaseChannel: string; setReleaseChannel: (v: string) => void explicitOrganization: boolean; setExplicitOrganization: (v: boolean) => void crashReporting: boolean; setCrashReporting: (v: boolean) => void analytics: boolean; setAnalytics: (v: boolean) => void } function SettingsBody(props: SettingsBodyProps) { return (
GitHub
Connect your GitHub account to clone and sync vaults.
Sync
Automatically pull vault changes from Git in the background.
Release Channel
Controls which features are visible. Alpha users see all features. Beta/Stable see features as they are promoted.
Privacy & Telemetry
Anonymous data helps us fix bugs and improve Laputa. No vault content, note titles, or file paths are ever sent.
) } function OrganizationWorkflowSection({ checked, onChange, }: { checked: boolean onChange: (value: boolean) => void }) { return ( <>
Workflow
Choose whether Laputa shows the Inbox workflow and the organized toggle.
) } function TelemetryToggle({ label, description, checked, onChange, testId }: { label: string; description: string; checked: boolean; onChange: (v: boolean) => void; testId: string }) { return ( ) } function SettingsFooter({ onClose, onSave }: { onClose: () => void; onSave: () => void }) { return (
{'\u2318'}, to open settings
) }