diff --git a/docs/src/components/api/auth-panel.tsx b/docs/src/components/api/auth-panel.tsx index 1847033d12..dd3c639ccd 100644 --- a/docs/src/components/api/auth-panel.tsx +++ b/docs/src/components/api/auth-panel.tsx @@ -3,12 +3,14 @@ import { AdminOwnedProject, CurrentInternalUser, useUser } from '@stackframe/stack'; import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises'; import { stringCompare } from '@stackframe/stack-shared/dist/utils/strings'; -import { AlertTriangle, ChevronDown, Key, X } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { AlertTriangle, Check, ChevronDown, X } from 'lucide-react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useSidebar } from '../layouts/sidebar-context'; import { Button } from '../mdx/button'; import { useAPIPageContext } from './api-page-wrapper'; +type AuthTab = 'select-project' | 'manual'; + type StackAuthHeaderKey = | 'X-Stack-Access-Type' | 'X-Stack-Project-Id' @@ -56,8 +58,32 @@ export function AuthPanel() { const projects = useMemo(() => ownedProjectsResult ?? [], [ownedProjectsResult]); const hasOwnedProjects = Boolean(internalUser); - // State for project selection + // State for project selection and tabs const [selectedProjectId, setSelectedProjectId] = useState(''); + const [activeTab, setActiveTab] = useState('select-project'); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + const mobileDropdownRef = useRef(null); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + const isOutsideDesktop = dropdownRef.current && !dropdownRef.current.contains(target); + const isOutsideMobile = mobileDropdownRef.current && !mobileDropdownRef.current.contains(target); + + // Only close if click is outside both dropdowns (or if the ref doesn't exist for that viewport) + if ( + (!dropdownRef.current || isOutsideDesktop) && + (!mobileDropdownRef.current || isOutsideMobile) + ) { + setIsDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); // Use default functions if sidebar context is not available const { isAuthOpen, toggleAuth } = sidebarContext || { @@ -141,7 +167,6 @@ export function AuthPanel() { // Calculate position based on homepage and scroll state (same as AIChatDrawer) const topPosition = isHomePage && isScrolled ? 'top-0' : 'top-0'; - const height = isHomePage && isScrolled ? 'h-screen' : 'h-[calc(100vh)]'; const missingRequiredHeaders = stackAuthHeaders.filter( header => header.required && !(headers[header.key] ?? '').trim() @@ -185,168 +210,204 @@ export function AuthPanel() { return ( <> - {/* Desktop Auth Panel - Matching AIChatDrawer design */} + {/* Desktop Auth Panel */}
- {/* Header - Matching AIChatDrawer */} -
-
-
- {highlightMissingHeaders ? ( - - ) : ( - - )} -
-
-

- {highlightMissingHeaders ? 'Authentication Required' : 'API Authentication'} -

-

- Configure headers for requests -

-
-
+ {/* Header */} +
+

+ API Authentication +

- {/* Error Message - Reserve space to prevent layout shifts */} -
- {highlightMissingHeaders && lastError ? ( -
-
- - - {lastError.status} Error - Authentication required - -
- {missingRequiredHeaders.length > 0 && ( -

- Missing: {missingRequiredHeaders.map(h => h.label).join(', ')} -

- )} -
- ) : null} + {/* Tabs */} +
+ +
- {/* Content - Fixed height to prevent layout shifts */} -
-
- {/* Project Selector - Show only if user has owned projects */} - {hasOwnedProjects && projects.length > 0 && ( -
- -
- - -
- {selectedProjectId && ( -

- ✓ Headers auto-populated for admin authentication -

- )} -
+ {/* Error Banner */} + {highlightMissingHeaders && lastError && ( +
+
+ + + {lastError.status} Error - Authentication required + +
+ {missingRequiredHeaders.length > 0 && ( +

+ Missing: {missingRequiredHeaders.map(h => h.label).join(', ')} +

)} +
+ )} + + {/* Content */} +
+ {activeTab === 'select-project' ? ( +
+ {hasOwnedProjects && projects.length > 0 ? ( + <> +
+ +
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {sortedProjects.map((project) => ( + + ))} +
+ )} +
+
- {/* Manual Header Inputs */} - {stackAuthHeaders.map((header) => { - // Hide certain fields when project is selected - if (selectedProjectId && header.hideWhenProjectSelected) { - return null; - } - - const value = headers[header.key] ?? ''; - const isMissing = highlightMissingHeaders && header.required && !value.trim(); - const isAutoPopulated = Boolean(header.isSensitive && selectedProjectId && value.length > 0); - - return ( -
- - updateSharedHeaders({ ...headers, [header.key]: e.target.value })} - readOnly={isAutoPopulated} - className={`w-full px-2 py-1.5 border rounded-md text-xs bg-fd-background text-fd-foreground placeholder:text-fd-muted-foreground focus:outline-none focus:ring-1 focus:border-transparent transition-all duration-200 ${ - isMissing - ? 'border-red-300 focus:ring-red-500 dark:border-red-700' - : 'border-fd-border focus:ring-fd-primary focus:border-fd-primary' - } ${isAutoPopulated ? 'bg-fd-muted/50 cursor-not-allowed' : ''}`} - /> - {isAutoPopulated && ( -

Auto-populated from your account

+ {selectedProjectId && ( +
+
+
+ + Ready to make requests + +
+

+ Because you're signed in, requests are automatically authenticated with your account. +

+
)} + + ) : ( +
+

+ Sign in to quickly select from your projects +

+

+ Or use the Manual tab to enter credentials directly +

- ); - })} -
+ )} +
+ ) : ( +
+ {stackAuthHeaders.map((header) => { + const value = headers[header.key] ?? ''; + const isMissing = highlightMissingHeaders && header.required && !value.trim(); + + return ( +
+ + updateSharedHeaders({ ...headers, [header.key]: e.target.value })} + className={`w-full px-3 py-2.5 border rounded-lg text-sm bg-fd-muted/50 text-fd-foreground placeholder:text-fd-muted-foreground focus:outline-none focus:ring-2 ${ + isMissing + ? 'border-red-300 dark:border-red-700 focus:ring-red-500' + : 'border-fd-border focus:ring-fd-primary focus:border-fd-primary' + }`} + /> +
+ ); + })} +
+ )}
{/* Footer Status */} -
-
+
+
v.trim()) + ? 'bg-green-500' + : 'bg-fd-muted-foreground' }`} /> - - {Object.values(headers).filter(v => v.trim()).length} configured - {selectedProjectId && ' (via project selection)'} + v.trim()) + ? 'text-green-600 dark:text-green-400' + : 'text-fd-muted-foreground' + }`}> + {missingRequiredHeaders.length === 0 && Object.values(headers).some(v => v.trim()) + ? 'Ready' + : 'Not configured'}
- {missingRequiredHeaders.length === 0 && Object.values(headers).some(v => v.trim()) && ( -
-
- - Ready for API requests - -
- )}
@@ -358,167 +419,201 @@ export function AuthPanel() { aria-hidden={!isAuthOpen} > {/* Mobile Header */} -
-
-
- {highlightMissingHeaders ? ( - - ) : ( - - )} -
-
-

- {highlightMissingHeaders ? 'Authentication Required' : 'API Authentication'} -

-

- Configure headers for requests -

-
-
+
+

+ API Authentication +

- {/* Error Message - Mobile */} -
- {highlightMissingHeaders && lastError ? ( -
-
- - - {lastError.status} Error - Authentication required - -
- {missingRequiredHeaders.length > 0 && ( -

- Missing: {missingRequiredHeaders.map(h => h.label).join(', ')} -

- )} -
- ) : null} + {/* Mobile Tabs */} +
+ +
- {/* Mobile Content - Fixed height to prevent layout shifts */} -
-
-
- {/* Project Selector - Mobile */} - {hasOwnedProjects && projects.length > 0 && ( -
- -
- - + {/* Mobile Error Banner */} + {highlightMissingHeaders && lastError && ( +
+
+ + + {lastError.status} Error - Authentication required + +
+ {missingRequiredHeaders.length > 0 && ( +

+ Missing: {missingRequiredHeaders.map(h => h.label).join(', ')} +

+ )} +
+ )} + + {/* Mobile Content */} +
+ {activeTab === 'select-project' ? ( +
+ {hasOwnedProjects && projects.length > 0 ? ( + <> +
+ +
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {sortedProjects.map((project) => ( + + ))} +
+ )} +
+ {selectedProjectId && ( -

- ✓ Headers auto-populated for admin authentication -

+
+
+
+ + Ready to make requests + +
+

+ Because you're signed in, requests are automatically authenticated with your account. +

+
)} + + ) : ( +
+

+ Sign in to quickly select from your projects +

+

+ Or use the Manual tab to enter credentials directly +

)} - - {/* Manual Header Inputs - Mobile */} +
+ ) : ( +
{stackAuthHeaders.map((header) => { - // Hide certain fields when project is selected - if (selectedProjectId && header.hideWhenProjectSelected) { - return null; - } - const value = headers[header.key] ?? ''; const isMissing = highlightMissingHeaders && header.required && !value.trim(); - const isAutoPopulated = Boolean(header.isSensitive && selectedProjectId && value.length > 0); return ( -
-
-
+ )}
{/* Mobile Footer */} -
+
-
+
v.trim()) + ? 'bg-green-500' + : 'bg-fd-muted-foreground' }`} /> - - {Object.values(headers).filter(v => v.trim()).length} configured - {selectedProjectId && ' (via project)'} + v.trim()) + ? 'text-green-600 dark:text-green-400' + : 'text-fd-muted-foreground' + }`}> + {missingRequiredHeaders.length === 0 && Object.values(headers).some(v => v.trim()) + ? 'Ready' + : 'Not configured'}
-
- {missingRequiredHeaders.length === 0 && Object.values(headers).some(v => v.trim()) && ( -
-
- - Ready for API requests - -
- )}