From da74c49e604b172fc617b52d7f1eb026d7244bc4 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 20 Jan 2026 14:29:22 -0600 Subject: [PATCH 1/3] actual api overhaul --- docs/src/components/api/auth-panel.tsx | 481 +++++++++++-------------- 1 file changed, 217 insertions(+), 264 deletions(-) diff --git a/docs/src/components/api/auth-panel.tsx b/docs/src/components/api/auth-panel.tsx index c8dfe98279..5e6963ffaa 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 { ChevronDown, X } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useSidebar } from '../layouts/sidebar-context'; import { useAPIPageContext } from './api-page-wrapper'; import { Button } from './button'; +type AuthTab = 'select-project' | 'manual'; + type StackAuthHeaderKey = | 'X-Stack-Access-Type' | 'X-Stack-Project-Id' @@ -56,8 +58,9 @@ 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'); // Use default functions if sidebar context is not available const { isAuthOpen, toggleAuth } = sidebarContext || { @@ -185,168 +188,144 @@ 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 -

- )} -
- )} - - {/* 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

+ {/* Content */} +
+ {activeTab === 'select-project' ? ( +
+ {hasOwnedProjects && projects.length > 0 ? ( + <> +
+ +
+ + +
+
+ + {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] ?? ''; + + 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 focus:ring-fd-primary focus:border-fd-primary border-fd-border" + /> +
+ ); + })} +
+ )}
{/* 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 +337,141 @@ 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 Content */} +
+ {activeTab === 'select-project' ? ( +
+ {hasOwnedProjects && projects.length > 0 ? ( + <> +
+ +
+ + +
+ {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 - -
- )}
From 634ac229166eecdd54e83da1315f99dabfb53b57 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 20 Jan 2026 14:34:57 -0600 Subject: [PATCH 2/3] add visual error --- docs/src/components/api/auth-panel.tsx | 192 ++++++++++++++++++++----- 1 file changed, 157 insertions(+), 35 deletions(-) diff --git a/docs/src/components/api/auth-panel.tsx b/docs/src/components/api/auth-panel.tsx index 5e6963ffaa..50333c46d0 100644 --- a/docs/src/components/api/auth-panel.tsx +++ b/docs/src/components/api/auth-panel.tsx @@ -3,8 +3,8 @@ 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 { ChevronDown, 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 { useAPIPageContext } from './api-page-wrapper'; import { Button } from './button'; @@ -61,6 +61,20 @@ export function AuthPanel() { // 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); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsDropdownOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); // Use default functions if sidebar context is not available const { isAuthOpen, toggleAuth } = sidebarContext || { @@ -211,7 +225,7 @@ export function AuthPanel() { {/* Tabs */}
+ {/* 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 ? ( <> -
+
- - + + {selectedProjectId + ? sortedProjects.find(p => p.id === selectedProjectId)?.displayName + : 'Select a project...'} + + + + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {sortedProjects.map((project) => ( + + ))} +
+ )}
@@ -288,18 +340,34 @@ export function AuthPanel() {
{stackAuthHeaders.map((header) => { const value = headers[header.key] ?? ''; + const isMissing = highlightMissingHeaders && header.required && !value.trim(); return (
-
); @@ -352,7 +420,7 @@ export function AuthPanel() { {/* Mobile Tabs */}
+ {/* 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' ? ( @@ -384,19 +469,40 @@ export function AuthPanel() { Select Project
- - + + {selectedProjectId + ? sortedProjects.find(p => p.id === selectedProjectId)?.displayName + : 'Select a project...'} + + + + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
+ {sortedProjects.map((project) => ( + + ))} +
+ )}
@@ -429,18 +535,34 @@ export function AuthPanel() {
{stackAuthHeaders.map((header) => { const value = headers[header.key] ?? ''; + const isMissing = highlightMissingHeaders && header.required && !value.trim(); return (
-
); From b7dfd1c9b1f13dc92d9a520d8c4ff7ede0d13b70 Mon Sep 17 00:00:00 2001 From: Madison Date: Tue, 27 Jan 2026 00:49:32 -0600 Subject: [PATCH 3/3] fix mobile dropdown and lint --- docs/src/components/api/auth-panel.tsx | 38 ++++++++++++++++++++------ 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/docs/src/components/api/auth-panel.tsx b/docs/src/components/api/auth-panel.tsx index 901e8ef523..dd3c639ccd 100644 --- a/docs/src/components/api/auth-panel.tsx +++ b/docs/src/components/api/auth-panel.tsx @@ -63,11 +63,20 @@ export function AuthPanel() { 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) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + 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); } }; @@ -158,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() @@ -225,7 +233,10 @@ export function AuthPanel() { {/* Tabs */}

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

)} @@ -420,7 +434,10 @@ export function AuthPanel() { {/* Mobile Tabs */}