Files
supabase/apps/docs/components/Navigation/NavigationMenu/NavigationMenuGuideList.tsx
Devanshu Sharma 36a4a774f7 refactor(docs): enhance the docs sidebar UI (#38869)
* refactor(docs): enhance NavigationMenuGuideListItems component

* cleanup..

* Resolve formatting conflicts in NavigationMenuGuideListItems

* Came up with a crazy recursive approach...

* some changes.

* More cleanups...

* more cleanups....

* formatting the stuff

* refactor(navigation): remove sample menu data and optimize icon rendering in RecursiveNavigation

* cleanup...

* feat: enhance navigation with recursive dropdown indicators and performance optimizations

- Add visual chevron indicators for expandable sidebar items
- Implement recursive navigation system using existing NavMenuSection types
- Auto-expand accordion sections containing current active page on load/refresh
- Extract LinkContainer as independent component to prevent recreation on renders
- Fix React 19 compatibility issues with ref handling using useCallback pattern
- Resolve duplicate key warnings with improved key generation strategy
- Optimize accordion transition timing (500ms duration with ease-in-out) to prevent element overlap
- Preserve existing icon logic with hasLightIcon support for theme variants
- Maintain backward compatibility with existing navigation data structures

Performance improvements:
- Memoized recursive components to prevent unnecessary re-renders
- Stable component references for better React reconciliation
- Proper TypeScript interfaces with explicit prop definitions

UX improvements:
- Smooth accordion animations with physics-based easing
- Clear visual indicators for expandable menu items
- Automatic expansion of sections containing current page
- Professional-grade transition animations

Technical details:
- Uses existing NavMenuSection recursive type structure
- Implements containsActivePath helper for active page detection
- Leverages Radix UI accordion with proper data-state attributes
- Maintains all existing functionality including dynamic menu injection

* Cleanup...

* feat: implement cookie-based persisted state for navigation accordion

- Replace localStorage with cookie-based persistence for SSR compatibility
- Add proper cookie helpers with SameSite=Lax and 30-day expiration
- Implement initialization state to prevent hydration mismatches
- Maintain auto-expand functionality for active page sections
- Preserve user's manual accordion state across page refreshes and sessions
- Add debounced cookie updates (300ms) for performance optimization
- Use proper cookie naming convention: 'supabase-docs-nav-state'

Benefits:
- SSR compatible: Works with server-side rendering
- Cross-session persistence: Maintains state across browser sessions
- Better security: Cookies are more secure than localStorage
- Performance: Debounced updates prevent excessive cookie writes
- User experience: Seamless navigation state preservation

* refactor: implement individual item-based persistence following reference pattern

- Replace global state management with individual item persistence
- Use sessionStorage with 'nav-expansion-' prefix for each item
- Follow the exact pattern from reference: usePersistedExpansionState hook
- Maintain auto-expansion for active page sections
- Simplify state management by removing complex global state
- Each accordion item manages its own expansion state independently
- Preserve user's manual toggle state across page refreshes
- Use sessionStorage instead of cookies for better performance

Benefits:
- Cleaner architecture: Each item manages its own state
- Better performance: No global state updates
- Simpler logic: Direct item-to-storage mapping
- Reference pattern compliance: Follows established patterns
- Individual control: Each section can be toggled independently
- Session persistence: Maintains state during browser session

* refactor: implement URL-driven navigation state following Supabase docs pattern

- Replace sessionStorage persistence with URL-driven expansion state
- Follow the established Supabase docs pattern: URL as single source of truth
- Remove individual item persistence hooks in favor of pathname-based logic
- Implement useUrlDrivenExpansion hook that determines open sections from current URL
- Use getSectionsContainingPath to find all sections that should be expanded
- Remove manual toggle functionality - sections open/close based on URL navigation
- Maintain smooth transitions and visual indicators for expandable sections
- Ensure consistency and reliability by using URL as the definitive state source

Benefits:
- Reliability: URL is always the single source of truth
- Consistency: Matches existing Supabase docs navigation behavior
- Simplicity: No complex state management or storage concerns
- Performance: No localStorage/sessionStorage operations
- SSR Compatible: Works perfectly with server-side rendering
- Predictable: Navigation state is always consistent with current page

* persistant state.

* file delete

* cleanup..

* file cleanup

* cleanup...

* cleanup..

* formatting..

* aww shit that it.

---------

Co-authored-by: Alan Daniel <stylesshjs@gmail.com>
2025-10-16 10:04:07 -04:00

105 lines
3.2 KiB
TypeScript

'use client'
import * as Accordion from '@radix-ui/react-accordion'
import { type NavMenuSection } from '../Navigation.types'
import * as NavItems from './NavigationMenu.constants'
import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
import { usePathname } from 'next/navigation'
import { PropsWithChildren } from 'react'
import { MenuId } from './NavigationMenu'
const NavigationMenuGuideList = ({
id,
additionalNavItems,
}: {
id: string
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
}) => {
const pathname = usePathname()
const firstLevelRoute = pathname?.split('/')?.slice(0, 4)?.join('/')
// eslint-disable-next-line import/namespace -- dynamic access, can't lint properly
let menu = NavItems[id]
if (id === MenuId.Integrations && additionalNavItems?.integrations) {
const integrationsListIndex = menu.items.findIndex((item) => item.name === 'Integrations')
if (integrationsListIndex !== -1) {
menu = {
...menu,
items: [
...menu.items.slice(0, integrationsListIndex),
{
...menu.items[integrationsListIndex],
items: [...menu.items[integrationsListIndex].items, ...additionalNavItems.integrations],
},
...menu.items.slice(integrationsListIndex + 1),
],
}
}
}
// Inject federated prompts into the 'AI Tools > Prompts' section
if (id === MenuId.GettingStarted && additionalNavItems?.prompts) {
const aiToolsSectionIndex = menu.items.findIndex((item) => item.name === 'AI Tools')
if (aiToolsSectionIndex !== -1) {
const beforeAITools = menu.items.slice(0, aiToolsSectionIndex)
const afterAITools = menu.items.slice(aiToolsSectionIndex + 1)
const aiToolsSection = menu.items[aiToolsSectionIndex]
const promptsSectionIndex = aiToolsSection.items.findIndex((item) => item.name === 'Prompts')
if (promptsSectionIndex !== -1) {
const beforePrompts = aiToolsSection.items.slice(0, promptsSectionIndex)
const afterPrompts = aiToolsSection.items.slice(promptsSectionIndex + 1)
const promptsSection = aiToolsSection.items[promptsSectionIndex]
const modifiedPromptsSection = {
...promptsSection,
items: additionalNavItems.prompts,
}
const modifiedAIToolsSection = {
...aiToolsSection,
items: [...beforePrompts, modifiedPromptsSection, ...afterPrompts],
}
menu = {
...menu,
items: [...beforeAITools, modifiedAIToolsSection, ...afterAITools],
}
}
}
}
return (
<NavigationMenuGuideListWrapper id={id} firstLevelRoute={firstLevelRoute}>
<NavigationMenuGuideListItems menu={menu} id={id} />
</NavigationMenuGuideListWrapper>
)
}
export function NavigationMenuGuideListWrapper({
id,
firstLevelRoute,
children,
}: PropsWithChildren<{
id: string
firstLevelRoute?: string
}>) {
return (
<Accordion.Root
collapsible={true}
key={id}
type="single"
value={firstLevelRoute}
className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150 w-full"
>
{children}
</Accordion.Root>
)
}
export default NavigationMenuGuideList