From 2c7e674c6c81dc24564a4f5f13939c9785ec6827 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Wed, 5 Nov 2025 17:13:55 -0600 Subject: [PATCH 01/22] close SearchMenu on client navigation --- packages/dev/s2-docs/src/SearchMenu.tsx | 16 ++++++++++++---- packages/dev/s2-docs/src/client.tsx | 2 ++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx index 001a0bdac1e..924bf1aca91 100644 --- a/packages/dev/s2-docs/src/SearchMenu.tsx +++ b/packages/dev/s2-docs/src/SearchMenu.tsx @@ -310,6 +310,18 @@ export function SearchMenu(props: SearchMenuProps) { || 'Items'; }, [filteredComponents, sections, selectedSectionId, searchValue]); + useEffect(() => { + const handleNavigation = () => { + setSearchValue(''); + onClose(); + }; + + window.addEventListener('rsc-navigation', handleNavigation); + return () => { + window.removeEventListener('rsc-navigation', handleNavigation); + }; + }, [onClose]); + return ( ) : ( { - setSearchValue(''); - onClose(); - }} items={selectedItems.map(item => ({ id: item.id, name: item.name, diff --git a/packages/dev/s2-docs/src/client.tsx b/packages/dev/s2-docs/src/client.tsx index a955a049d6f..27d7fdaba5a 100644 --- a/packages/dev/s2-docs/src/client.tsx +++ b/packages/dev/s2-docs/src/client.tsx @@ -35,6 +35,8 @@ async function navigate(pathname: string, push = false) { element.scrollIntoView(); } } + + window.dispatchEvent(new CustomEvent('rsc-navigation')); }); } catch { let errorRes = await fetchRSC('/error.rsc'); From 5cb61c3ae43398023096d51a4ba607f58cdaed49 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 6 Nov 2025 10:26:42 -0600 Subject: [PATCH 02/22] add footer --- packages/dev/s2-docs/src/Layout.tsx | 66 +++++++++++++++++++++++------ packages/dev/s2-docs/src/footer.css | 8 ++++ 2 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 packages/dev/s2-docs/src/footer.css diff --git a/packages/dev/s2-docs/src/Layout.tsx b/packages/dev/s2-docs/src/Layout.tsx index fb4d5caca28..3d315d50feb 100644 --- a/packages/dev/s2-docs/src/Layout.tsx +++ b/packages/dev/s2-docs/src/Layout.tsx @@ -9,11 +9,13 @@ import internationalizedFavicon from 'url:../assets/internationalized.ico'; // @ts-ignore import reactAriaFavicon from 'url:../assets/react-aria.ico'; import './anatomy.css'; +import './footer.css'; import ChevronRightIcon from '@react-spectrum/s2/icons/ChevronRight'; import {ClassAPI} from './ClassAPI'; import {Code} from './Code'; import {CodeBlock} from './CodeBlock'; import {CodePlatterProvider} from './CodePlatter'; +import {Divider, PickerItem, Provider} from '@react-spectrum/s2'; import {ExampleSwitcher} from './ExampleSwitcher'; import {getLibraryFromPage, getLibraryFromUrl, getLibraryLabel} from './library'; import {getTextWidth} from './textWidth'; @@ -22,7 +24,6 @@ import Header from './Header'; import {iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Link, TitleLink} from './Link'; import {MobileHeader} from './MobileHeader'; -import {PickerItem, Provider} from '@react-spectrum/s2'; import {PropTable} from './PropTable'; import {StateTable} from './StateTable'; import {TypeLink} from './types'; @@ -139,6 +140,36 @@ let articleStyles = style({ height: 'fit' }); +function Footer() { + const year = new Date().getFullYear(); + return ( +
+ +
    +
  • Copyright © {year} Adobe. All rights reserved.
  • +
  • Privacy
  • +
  • Terms of Use
  • +
  • Cookies
  • +
  • Do not sell my personal information
  • +
+
+ ); +} + export function Layout(props: PageProps & {children: ReactElement}) { let {pages, currentPage, children} = props; let hasToC = !currentPage.exports?.hideNav && currentPage.tableOfContents?.[0]?.children && currentPage.tableOfContents?.[0]?.children?.length > 0; @@ -257,18 +288,27 @@ export function Layout(props: PageProps & {children: ReactElement}) { lg: 'auto' } })}> - -
- {currentPage.exports?.version && } - {React.cloneElement(children, { - components: isSubpage ? - subPageComponents(parentPage) : - components, - pages - })} -
-
+
+ +
+ {currentPage.exports?.version && } + {React.cloneElement(children, { + components: isSubpage ? + subPageComponents(parentPage) : + components, + pages + })} +
+
+
+
{hasToC && } @@ -340,14 +352,14 @@ export function Layout(props: PageProps & {children: ReactElement}) { ); } -function Toc({toc}) { +function Toc({toc, isNested = false}) { return ( - + {toc.map((c, i) => ( {c.title} - {c.children.length > 0 && } + {c.children.length > 0 && } ))} diff --git a/packages/dev/s2-docs/src/MarkdownMenu.tsx b/packages/dev/s2-docs/src/MarkdownMenu.tsx index e1973197437..6dab526c79c 100644 --- a/packages/dev/s2-docs/src/MarkdownMenu.tsx +++ b/packages/dev/s2-docs/src/MarkdownMenu.tsx @@ -1,9 +1,12 @@ 'use client'; import {ActionButton, Menu, MenuItem, MenuTrigger, Text, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2'; +import CheckmarkCircle from '@react-spectrum/s2/icons/CheckmarkCircle'; import Copy from '@react-spectrum/s2/icons/Copy'; -import OpenIn from '@react-spectrum/s2/icons/OpenIn'; -import React, {useCallback} from 'react'; +import {getLibraryFromUrl, getLibraryLabel} from './library'; +import More from '@react-spectrum/s2/icons/More'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; interface MarkdownMenuProps { url: string | undefined @@ -11,54 +14,63 @@ interface MarkdownMenuProps { export function MarkdownMenu({url}: MarkdownMenuProps) { let mdUrl = (url ?? '').replace(/\.html?$/i, '') + '.md'; + let [isCopied, setIsCopied] = useState(false); + let timeout = useRef | null>(null); + + let pageUrl = typeof window !== 'undefined' && url ? new URL(url, window.location.origin).href : url ?? ''; + let fullMdUrl = typeof window !== 'undefined' && mdUrl ? new URL(mdUrl, window.location.origin).href : mdUrl; + let library = url ? getLibraryLabel(getLibraryFromUrl(url)) : ''; + let aiPrompt = `Answer questions about the following ${library} documentation page: ${pageUrl}\nMarkdown source: ${fullMdUrl}`; + let chatGptUrl = `https://chatgpt.com/?q=${encodeURIComponent(aiPrompt)}`; + let claudeUrl = `https://claude.ai/new?q=${encodeURIComponent(aiPrompt)}`; - let onAction = useCallback(async (key: import('react-aria-components').Key) => { - let action = String(key); - switch (action) { - case 'copy': { - if (typeof navigator !== 'undefined' && navigator.clipboard) { - try { - let response = await fetch(mdUrl); - // Fallback to copying the URL if the request fails or isn't ok. - if (!response.ok) { - throw new Error('Failed to fetch markdown'); - } - - let markdown = await response.text(); - await navigator.clipboard.writeText(markdown); - } catch (error) { - // Show toast for clipboard errors, but silently ignore fetch errors - if (error instanceof Error && error.name !== 'TypeError') { - ToastQueue.negative('Failed to copy markdown.'); - } - } - } - break; + useEffect(() => { + return () => { + if (timeout.current) { + clearTimeout(timeout.current); } - case 'view': { - if (typeof window !== 'undefined') { - window.open(mdUrl, '_blank'); - } - break; + }; + }, []); + + let handleCopy = useCallback(async () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + if (typeof navigator !== 'undefined' && navigator.clipboard) { + try { + let response = await fetch(mdUrl); + let markdown = await response.text(); + await navigator.clipboard.writeText(markdown); + setIsCopied(true); + timeout.current = setTimeout(() => setIsCopied(false), 2000); + } catch { + ToastQueue.negative('Failed to copy markdown.'); } } }, [mdUrl]); return ( - - - +
+ + {isCopied ? : } + Copy Page - - - - Copy Page as Markdown - - - - View Page as Markdown - - - + + + + + + + View as Markdown + + + Open in ChatGPT + + + Open in Claude + + + +
); } diff --git a/packages/dev/s2-docs/src/Nav.tsx b/packages/dev/s2-docs/src/Nav.tsx index 96ee1872f10..9c429591add 100644 --- a/packages/dev/s2-docs/src/Nav.tsx +++ b/packages/dev/s2-docs/src/Nav.tsx @@ -110,7 +110,7 @@ function SideNavSection({title, children}) { const SideNavContext = createContext(''); -export function SideNav({children}) { +export function SideNav({children, isNested = false}) { return (
    ul)': 16 }, + paddingTop: { + default: 0, + isNested: 8 + }, margin: 0, display: 'flex', flexDirection: 'column', gap: 8, width: 'full', boxSizing: 'border-box' - })}> + })({isNested})}> {children}
); diff --git a/packages/dev/s2-docs/src/ScrollableToc.tsx b/packages/dev/s2-docs/src/ScrollableToc.tsx new file mode 100644 index 00000000000..7fb8dcba7a6 --- /dev/null +++ b/packages/dev/s2-docs/src/ScrollableToc.tsx @@ -0,0 +1,45 @@ +'use client'; + +import React, {useEffect, useRef, useState} from 'react'; +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; + +export function ScrollableToc({children}) { + let [topMaskSize, setTopMaskSize] = useState(0); + let [bottomMaskSize, setBottomMaskSize] = useState(0); + let scrollRef = useRef(null); + + let updateMasks = (element: HTMLDivElement) => { + let scrollTop = element.scrollTop; + let scrollHeight = element.scrollHeight; + let clientHeight = element.clientHeight; + let distanceFromBottom = scrollHeight - scrollTop - clientHeight; + + setTopMaskSize(Math.min(scrollTop, 32)); + setBottomMaskSize(Math.min(distanceFromBottom, 32)); + }; + + useEffect(() => { + if (scrollRef.current) { + updateMasks(scrollRef.current); + } + }, [children]); + + return ( +
updateMasks(e.currentTarget)} + style={{ + maskImage: [ + topMaskSize > 0 ? `linear-gradient(to bottom, transparent, black ${topMaskSize}px)` : null, + bottomMaskSize > 0 ? `linear-gradient(to top, transparent, black ${bottomMaskSize}px)` : null + ].filter(Boolean).join(', ') || undefined + }} + className={style({ + overflowY: 'auto', + flex: 1, + minHeight: 0 + })}> + {children} +
+ ); +} From c35adb7d5e95d9edeb96d5696e1ffc7cd19c5f32 Mon Sep 17 00:00:00 2001 From: Reid Barber Date: Thu, 6 Nov 2025 16:48:44 -0600 Subject: [PATCH 09/22] add skeleton loading for client routing --- packages/dev/s2-docs/src/Layout.tsx | 23 ++-- .../dev/s2-docs/src/NavigationSuspense.tsx | 70 ++++++++++ packages/dev/s2-docs/src/PageSkeleton.tsx | 128 ++++++++++++++++++ packages/dev/s2-docs/src/client.tsx | 50 ++++--- 4 files changed, 241 insertions(+), 30 deletions(-) create mode 100644 packages/dev/s2-docs/src/NavigationSuspense.tsx create mode 100644 packages/dev/s2-docs/src/PageSkeleton.tsx diff --git a/packages/dev/s2-docs/src/Layout.tsx b/packages/dev/s2-docs/src/Layout.tsx index a601c40f37a..18df8735bcb 100644 --- a/packages/dev/s2-docs/src/Layout.tsx +++ b/packages/dev/s2-docs/src/Layout.tsx @@ -25,6 +25,7 @@ import {iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'}; import {Link, TitleLink} from './Link'; import {MarkdownMenu} from './MarkdownMenu'; import {MobileHeader} from './MobileHeader'; +import {NavigationSuspense} from './NavigationSuspense'; import {PropTable} from './PropTable'; import {ScrollableToc} from './ScrollableToc'; import {StateTable} from './StateTable'; @@ -302,16 +303,18 @@ export function Layout(props: PageProps & {children: ReactElement}) { width: 'full' })}> -
- {currentPage.exports?.version && } - {React.cloneElement(children, { - components: isSubpage ? - subPageComponents(parentPage) : - components, - pages - })} -
+ +
+ {currentPage.exports?.version && } + {React.cloneElement(children, { + components: isSubpage ? + subPageComponents(parentPage) : + components, + pages + })} +
+