Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2c7e674
close SearchMenu on client navigation
reidbarber Nov 5, 2025
5cb61c3
add footer
reidbarber Nov 6, 2025
33f0724
made Nav and ToC widths static
reidbarber Nov 6, 2025
35069c4
fix min-height issue on mobile
reidbarber Nov 6, 2025
5e84727
show Toast if route can't load
reidbarber Nov 6, 2025
9122573
add toast for clipboard copy failures
reidbarber Nov 6, 2025
45ec5f7
fix React insertion effect
reidbarber Nov 6, 2025
81f27fb
improve markdown menu
reidbarber Nov 6, 2025
c35adb7
add skeleton loading for client routing
reidbarber Nov 6, 2025
c7b2c20
copy update
reidbarber Nov 6, 2025
217f3df
Merge remote-tracking branch 'origin/main' into s2-docs-general-impro…
reidbarber Nov 6, 2025
df6fe71
remote extra startTransitions and use use hook
reidbarber Nov 7, 2025
2bbe51f
improve skeleton loading and optimistic render link selection + ToC
reidbarber Nov 7, 2025
93dca54
add delay to showing skeleton
reidbarber Nov 7, 2025
9338497
fix getPageInfo logic
reidbarber Nov 7, 2025
dc3309a
try fixing getPageInfo again
reidbarber Nov 7, 2025
25297d3
try fixing normalizePathname on build
reidbarber Nov 7, 2025
aa6ba15
add prefetch onPressStart
reidbarber Nov 7, 2025
9cd4e02
revert optimistic UI and show error toast if fetch fails
reidbarber Nov 7, 2025
fbd63dd
fix skeleton title
reidbarber Nov 10, 2025
31683a3
don't clear targetPathname until new navigation
reidbarber Nov 10, 2025
4970161
move prefetch to a global pointerover listener
reidbarber Nov 10, 2025
cf27fd3
close search menu when navigation starts
reidbarber Nov 10, 2025
8d77e46
show skeleton loading immediately when navigation starts
reidbarber Nov 10, 2025
de49692
Merge remote-tracking branch 'origin/main' into s2-docs-general-impro…
reidbarber Nov 11, 2025
d971a15
re-use tag group in SearchMenu and MobileSearchMenu
reidbarber Nov 11, 2025
7d2fb80
fix and extract out search logic
reidbarber Nov 11, 2025
5cfdd17
remove blog and releases index pages from search
reidbarber Nov 11, 2025
6c98a92
reduce size of icons in dnd blog post example
reidbarber Nov 11, 2025
537d8b1
fix Forms password example
reidbarber Nov 11, 2025
751fc05
centralize search logic
reidbarber Nov 11, 2025
77dd142
add escapeKeyBehavior="none" to tag groups
reidbarber Nov 11, 2025
0a7dcbc
fix toc scroll mask
reidbarber Nov 11, 2025
f4fd084
simplify pathname logic
reidbarber Nov 12, 2025
67a3b66
remove promises from prefetch cache once resolved
reidbarber Nov 12, 2025
aef8e3b
use useOptimistic
reidbarber Nov 12, 2025
f3b9f25
use fetchRSC promise as loading source
reidbarber Nov 12, 2025
839f0d3
avoid navigation race conditions
reidbarber Nov 12, 2025
80431b0
prefetch on focus (same as pointerover)
reidbarber Nov 12, 2025
512a045
function for checking if client link
reidbarber Nov 12, 2025
90abcd8
fix search results sorting for All tag
reidbarber Nov 12, 2025
b70d7d8
add message to currentAbortController.abort
reidbarber Nov 12, 2025
d01efc1
lint
reidbarber Nov 12, 2025
ff3c90c
Merge remote-tracking branch 'origin/main' into s2-docs-general-impro…
reidbarber Nov 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/dev/s2-docs/src/CodePlatter.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import {ActionButton, ActionButtonGroup, Button, ButtonGroup, Content, createIcon, Dialog, DialogContainer, Heading, Link, Menu, MenuItem, MenuTrigger, Text, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import {ActionButton, ActionButtonGroup, Button, ButtonGroup, Content, createIcon, Dialog, DialogContainer, Heading, Link, Menu, MenuItem, MenuTrigger, Text, UNSTABLE_ToastQueue as ToastQueue, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import {CopyButton} from './CopyButton';
import {createCodeSandbox, getCodeSandboxFiles} from './CodeSandbox';
import {createStackBlitz} from './StackBlitz';
Expand Down Expand Up @@ -97,7 +97,9 @@ export function CodePlatter({children, shareUrl, files, type, registryUrl, showC
if (node instanceof HTMLHeadingElement && node.id) {
url.hash = '#' + node.id;
}
navigator.clipboard.writeText(url.toString());
navigator.clipboard.writeText(url.toString()).catch(() => {
ToastQueue.negative('Failed to copy link.');
});
}}>
<LinkIcon />
<Text slot="label">Copy link</Text>
Expand Down Expand Up @@ -265,7 +267,9 @@ function ShadcnDialog({registryUrl}) {
<Button
variant="accent"
onPress={() => {
navigator.clipboard.writeText(preRef.current!.textContent!);
navigator.clipboard.writeText(preRef.current!.textContent!).catch(() => {
ToastQueue.negative('Failed to copy command. Please try again.');
});
close();
}}>
Copy and close
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/src/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import {ActionButton, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import {ActionButton, UNSTABLE_ToastQueue as ToastQueue, Tooltip, TooltipTrigger} from '@react-spectrum/s2';
import CheckmarkCircle from '@react-spectrum/s2/icons/CheckmarkCircle';
import Copy from '@react-spectrum/s2/icons/Copy';
import React, {useEffect, useRef, useState} from 'react';
Expand Down Expand Up @@ -44,7 +44,7 @@ export function CopyButton({text, getText, ariaLabel = 'Copy', tooltip = 'Copy',
setIsCopied(true);
timeout.current = setTimeout(() => setIsCopied(false), 2000);
}).catch(() => {
// noop
ToastQueue.negative('Failed to copy.');
});
};

Expand Down
2 changes: 0 additions & 2 deletions packages/dev/s2-docs/src/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import BetaApp from '@react-spectrum/s2/icons/BetaApp';
import {flushSync} from 'react-dom';
import {getLibraryFromPage, getLibraryIcon, getLibraryLabel} from './library';
import GithubLogo from './icons/GithubLogo';
import {MarkdownMenu} from './MarkdownMenu';
// @ts-ignore
import {PageProps} from '@parcel/rsc';
import React, {CSSProperties, useId, useState} from 'react';
Expand Down Expand Up @@ -115,7 +114,6 @@ export default function Header(props: PageProps) {
<BetaApp />
<Text>Beta Preview</Text>
</Badge>
<MarkdownMenu url={currentPage.url} />
<ActionButton aria-label="React Spectrum GitHub repo" size="L" isQuiet onPress={() => window.open('https://github.com/adobe/react-spectrum', '_blank', 'noopener,noreferrer')}>
<GithubLogo />
</ActionButton>
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/src/IconSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import {Autocomplete, GridLayout, ListBox, ListBoxItem, Size, useFilter, Virtualizer} from 'react-aria-components';
import CheckmarkCircle from '@react-spectrum/s2/icons/CheckmarkCircle';
import Close from '@react-spectrum/s2/icons/Close';
import {Content, Heading, IllustratedMessage, pressScale, SearchField, Skeleton, Text} from '@react-spectrum/s2';
import {Content, Heading, IllustratedMessage, pressScale, SearchField, Skeleton, Text, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2';
import {focusRing, iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {iconAliases} from './iconAliases.js';
// @ts-ignore
Expand Down Expand Up @@ -50,7 +50,7 @@ export function useCopyImport() {
setCopiedId(id);
timeout.current = setTimeout(() => setCopiedId(null), 2000);
}).catch(() => {
// noop
ToastQueue.negative('Failed to copy import statement.');
});
}, []);

Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/src/IllustrationCards.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import {Autocomplete, GridLayout, ListBox, ListBoxItem, Size, useFilter, Virtualizer} from 'react-aria-components';
import {Content, Heading, IllustratedMessage, pressScale, ProgressCircle, Radio, RadioGroup, SearchField, SegmentedControl, SegmentedControlItem, Text} from '@react-spectrum/s2';
import {Content, Heading, IllustratedMessage, pressScale, ProgressCircle, Radio, RadioGroup, SearchField, SegmentedControl, SegmentedControlItem, Text, UNSTABLE_ToastQueue as ToastQueue} from '@react-spectrum/s2';
import {focusRing, style} from '@react-spectrum/s2/style' with {type: 'macro'};
// @ts-ignore
import Gradient from '@react-spectrum/s2/icons/Gradient';
Expand Down Expand Up @@ -100,7 +100,7 @@ let handleCopyImport = (id: string, variant: string, gradientStyle: string) => {
navigator.clipboard.writeText(importText).then(() => {
// noop
}).catch(() => {
// noop
ToastQueue.negative('Failed to copy import statement.');
});
};

Expand Down
109 changes: 85 additions & 24 deletions packages/dev/s2-docs/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,25 @@ 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, UNSTABLE_ToastContainer as ToastContainer} from '@react-spectrum/s2';
import {ExampleSwitcher} from './ExampleSwitcher';
import {getLibraryFromPage, getLibraryFromUrl, getLibraryLabel} from './library';
import {getTextWidth} from './textWidth';
import {H2, H3, H4} from './Headings';
import Header from './Header';
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 {PickerItem, Provider} from '@react-spectrum/s2';
import {NavigationSuspense} from './NavigationSuspense';
import {PropTable} from './PropTable';
import {ScrollableToc} from './ScrollableToc';
import {StateTable} from './StateTable';
import {TypeLink} from './types';
import {VersionBadge} from './VersionBadge';
Expand Down Expand Up @@ -139,6 +143,36 @@ let articleStyles = style({
height: 'fit'
});

function Footer() {
const year = new Date().getFullYear();
return (
<footer
className={style({
marginTop: 32,
paddingY: 12
})}>
<Divider size="S" />
<ul
className={style({
display: 'flex',
justifyContent: 'end',
flexWrap: 'wrap',
paddingX: 12,
margin: 0,
marginTop: 16,
font: 'body-2xs',
listStyleType: 'none'
})}>
<li>Copyright © {year} Adobe. All rights reserved.</li>
<li><Link isQuiet href="//www.adobe.com/privacy.html" variant="secondary">Privacy</Link></li>
<li><Link isQuiet href="//www.adobe.com/legal/terms.html" variant="secondary">Terms of Use</Link></li>
<li><Link isQuiet href="//www.adobe.com/privacy/cookies.html" variant="secondary">Cookies</Link></li>
<li><Link isQuiet href="//www.adobe.com/privacy/ca-rights.html" variant="secondary">Do not sell my personal information</Link></li>
</ul>
</footer>
);
}

export function Layout(props: PageProps & {children: ReactElement<any>}) {
let {pages, currentPage, children} = props;
let hasToC = !currentPage.exports?.hideNav && currentPage.tableOfContents?.[0]?.children && currentPage.tableOfContents?.[0]?.children?.length > 0;
Expand Down Expand Up @@ -218,14 +252,18 @@ export function Layout(props: PageProps & {children: ReactElement<any>}) {
gap: {
default: 0,
lg: 12
},
minHeight: {
default: 'screen',
lg: 'auto'
}
})}>
<Header pages={pages} currentPage={currentPage} />
<MobileHeader
toc={(currentPage.tableOfContents?.[0]?.children?.length ?? 0) > 1 ? <MobileToc key="toc" toc={currentPage.tableOfContents ?? []} currentPage={currentPage} /> : null}
pages={pages}
currentPage={currentPage} />
<div className={style({display: 'flex', width: 'full'})}>
<div className={style({display: 'flex', width: 'full', flexGrow: {default: 1, lg: 0}})}>
{currentPage.exports?.hideNav ? null : <Nav pages={pages} currentPage={currentPage} />}
<main
key={currentPage.url}
Expand Down Expand Up @@ -257,51 +295,74 @@ export function Layout(props: PageProps & {children: ReactElement<any>}) {
lg: 'auto'
}
})}>
<CodePlatterProvider library={getLibraryFromUrl(currentPage.url)}>
<article
className={articleStyles({isWithToC: hasToC})}>
{currentPage.exports?.version && <VersionBadge version={currentPage.exports.version} />}
{React.cloneElement(children, {
components: isSubpage ?
subPageComponents(parentPage) :
components,
pages
})}
</article>
</CodePlatterProvider>
<div
className={style({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
width: 'full'
})}>
<CodePlatterProvider library={getLibraryFromUrl(currentPage.url)}>
<NavigationSuspense pages={pages}>
<article
className={articleStyles({isWithToC: hasToC})}>
{currentPage.exports?.version && <VersionBadge version={currentPage.exports.version} />}
{React.cloneElement(children, {
components: isSubpage ?
subPageComponents(parentPage) :
components,
pages
})}
</article>
</NavigationSuspense>
</CodePlatterProvider>
<Footer />
</div>
{hasToC && <aside
className={style({
position: 'sticky',
top: 0,
height: 'fit',
maxHeight: 'screen',
overflow: 'auto',
height: {
default: 'fit',
lg: '[calc(100vh - 72px)]'
},
paddingY: 32,
paddingX: 4,
boxSizing: 'border-box',
width: 180,
flexShrink: 0,
display: {
default: 'none',
lg: 'block'
}
lg: 'flex'
},
flexDirection: 'column'
})}>
<div className={style({font: 'title', minHeight: 32, paddingX: 12, display: 'flex', alignItems: 'center'})}>Contents</div>
<Toc toc={currentPage.tableOfContents?.[0]?.children ?? []} />
<div className={style({font: 'title', minHeight: 32, paddingX: 12, display: 'flex', alignItems: 'center', marginBottom: 4, flexShrink: 0})}>On this page</div>
<ScrollableToc>
<Toc toc={currentPage.tableOfContents?.[0]?.children ?? []} />
</ScrollableToc>
<div className={style({flexShrink: 0})}>
<Divider size="S" styles={style({marginY: 12})} />
<MarkdownMenu url={currentPage.url} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for the team: do we prefer this at the bottom of the page or at the top (i.e. above "on this page")? Most sites seem to have it at the top. I don't mind the bottom but it's a little less prominent. Also moves around a bit depending on the length of the toc.

Copy link
Member Author

@reidbarber reidbarber Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My reasoning was that the table of contents is more useful most of the time, so it's better when scanning to read that first. Also if we end up adding more actions (feedback button, etc.) it might crowd the top. Definitely open to changing this though.

Also for pages with many sections it renders at the bottom and the ToC has scrolling.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO I like it at the bottom, but it really depends on how prominent we want to make it

</div>
</aside>}
</main>
</div>
</div>
<ToastContainer placement="bottom" />
</body>
</Provider>
);
}

function Toc({toc}) {
function Toc({toc, isNested = false}) {
return (
<OnPageNav>
<SideNav>
<SideNav isNested={isNested}>
{toc.map((c, i) => (
<SideNavItem key={i}>
<SideNavLink href={'#' + anchorId(c.title)}>{c.title}</SideNavLink>
{c.children.length > 0 && <Toc toc={c.children} />}
{c.children.length > 0 && <Toc toc={c.children} isNested />}
</SideNavItem>
))}
</SideNav>
Expand Down
97 changes: 56 additions & 41 deletions packages/dev/s2-docs/src/MarkdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,76 @@
'use client';

import {ActionButton, Menu, MenuItem, MenuTrigger, Text} from '@react-spectrum/s2';
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
}

export function MarkdownMenu({url}: MarkdownMenuProps) {
let mdUrl = (url ?? '').replace(/\.html?$/i, '') + '.md';
let [isCopied, setIsCopied] = useState(false);
let timeout = useRef<ReturnType<typeof setTimeout> | 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 {
// ignore failures (e.g., insecure context, network errors)
}
}
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 (
<MenuTrigger>
<ActionButton size="L" isQuiet aria-label="Markdown options">
<Copy />
<div className={style({display: 'flex', justifyContent: 'space-between', paddingX: 4, paddingBottom: 16})}>
<ActionButton isQuiet size="M" onPress={handleCopy}>
{isCopied ? <CheckmarkCircle /> : <Copy />}
<Text>Copy for LLM</Text>
</ActionButton>
<Menu onAction={onAction}>
<MenuItem id="copy">
<Copy />
<Text>Copy Page as Markdown</Text>
</MenuItem>
<MenuItem id="view">
<OpenIn />
<Text>View Page as Markdown</Text>
</MenuItem>
</Menu>
</MenuTrigger>
<MenuTrigger>
<ActionButton size="M" isQuiet aria-label="Markdown options">
<More />
</ActionButton>
<Menu>
<MenuItem id="view" href={mdUrl} target="_blank">
View as Markdown
</MenuItem>
<MenuItem id="chatgpt" href={chatGptUrl} target="_blank">
Open in ChatGPT
</MenuItem>
<MenuItem id="claude" href={claudeUrl} target="_blank">
Open in Claude
</MenuItem>
</Menu>
</MenuTrigger>
</div>
);
}
Loading