diff --git a/packages/dev/s2-docs/pages/react-aria/getting-started.mdx b/packages/dev/s2-docs/pages/react-aria/getting-started.mdx
index 37f581a7c33..22218e209a9 100644
--- a/packages/dev/s2-docs/pages/react-aria/getting-started.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/getting-started.mdx
@@ -9,6 +9,7 @@ import {ShadcnCommand} from '../../src/ShadcnCommand';
import {StarterKits} from '../../src/StarterKits';
import docs from 'docs:react-aria-components';
import '../../tailwind/tailwind.css';
+import {Link} from '@react-spectrum/s2';
export const section = 'Getting started';
export const tags = ['introduction', 'installation'];
@@ -73,7 +74,7 @@ If you're building a full component library, download a pre-built [Storybook](ht
### Working with AI
-Use the menu at the top of each page in the docs to open or copy it into your favorite AI assistant. We also have an [MCP server](mcp.html) which can be used directly in your IDE, and [llms.txt](../llms.txt) which can help AI agents navigate the docs.
+Use the menu at the top of each page in the docs to open or copy it into your favorite AI assistant. We also have an [MCP server](mcp.html) which can be used directly in your IDE, and lms.txt which can help AI agents navigate the docs.
## Build a component from scratch
diff --git a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx
index 1f7e1c93c53..ce1dbeeebb8 100644
--- a/packages/dev/s2-docs/pages/react-aria/useDrop.mdx
+++ b/packages/dev/s2-docs/pages/react-aria/useDrop.mdx
@@ -198,7 +198,7 @@ function DropTarget() {
);
}
return (
-
+
{contents}
);
@@ -309,7 +309,7 @@ function DropTarget() {
onDrop: onEvent
});
return (
-
+
{events.map((e, i) => {e} )}
);
diff --git a/packages/dev/s2-docs/pages/s2/Icons.mdx b/packages/dev/s2-docs/pages/s2/Icons.mdx
index 3c1d22a6f70..b0728970116 100644
--- a/packages/dev/s2-docs/pages/s2/Icons.mdx
+++ b/packages/dev/s2-docs/pages/s2/Icons.mdx
@@ -1,7 +1,7 @@
import {Layout} from '../../src/Layout';
import {InstallCommand} from '../../src/InstallCommand';
import {Command} from '../../src/Command';
-import {IconCards} from '../../src/IconSearchView';
+import {IconsPageSearch} from '../../src/IconSearchView';
import {IconColors} from '../../src/IconColors';
import {IconSizes} from '../../src/IconSizes';
import {InlineAlert, Heading, Content} from '@react-spectrum/s2';
@@ -24,7 +24,7 @@ import Edit from "@react-spectrum/s2/icons/Edit";
## Available icons
-
+
## API
diff --git a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs
index f9238c5299a..3f2d14d5c2c 100644
--- a/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs
+++ b/packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs
@@ -727,13 +727,13 @@ function remarkDocsComponentsToMarkdown() {
}
// Render an unordered list of icon names.
- if (name === 'IconCards') {
+ if (name === 'IconsPageSearch') {
const iconList = getIconNames();
const listMarkdown = iconList.length
? iconList.map(iconName => `- ${iconName}`).join('\n')
: '> Icon list could not be generated.';
- const iconCardsNode = unified().use(remarkParse).parse(listMarkdown);
- parent.children.splice(index, 1, ...iconCardsNode.children);
+ const iconListNode = unified().use(remarkParse).parse(listMarkdown);
+ parent.children.splice(index, 1, ...iconListNode.children);
return index;
}
diff --git a/packages/dev/s2-docs/src/IconSearchView.tsx b/packages/dev/s2-docs/src/IconSearchView.tsx
index fae26461399..da720249066 100644
--- a/packages/dev/s2-docs/src/IconSearchView.tsx
+++ b/packages/dev/s2-docs/src/IconSearchView.tsx
@@ -2,18 +2,99 @@
'use client';
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 {focusRing, iconStyle, style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {iconAliases} from './iconAliases.js';
// @ts-ignore
import icons from '/packages/@react-spectrum/s2/s2wf-icons/*.svg';
+import InfoCircle from '@react-spectrum/s2/icons/InfoCircle';
// eslint-disable-next-line monorepo/no-internal-import
import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults';
-import React, {useCallback, useMemo, useRef} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
export const iconList = Object.keys(icons).map(name => ({id: name.replace(/^S2_Icon_(.*?)(Size\d+)?_2.*/, '$1'), icon: icons[name].default}));
+export function useIconFilter() {
+ let {contains} = useFilter({sensitivity: 'base'});
+ return useCallback((textValue: string, inputValue: string) => {
+ // Check for alias matches
+ for (const alias of Object.keys(iconAliases)) {
+ if (contains(alias, inputValue) && iconAliases[alias].includes(textValue)) {
+ return true;
+ }
+ }
+ // Also compare for substrings in the icon's actual name
+ return textValue != null && contains(textValue, inputValue);
+ }, [contains]);
+}
+
+export function useCopyImport() {
+ let [copiedId, setCopiedId] = useState(null);
+ let timeout = useRef | null>(null);
+
+ useEffect(() => {
+ return () => {
+ if (timeout.current) {
+ clearTimeout(timeout.current);
+ }
+ };
+ }, []);
+
+ let handleCopyImport = useCallback((id: string) => {
+ if (timeout.current) {
+ clearTimeout(timeout.current);
+ }
+ navigator.clipboard.writeText(`import ${id} from '@react-spectrum/s2/icons/${id}';`).then(() => {
+ setCopiedId(id);
+ timeout.current = setTimeout(() => setCopiedId(null), 2000);
+ }).catch(() => {
+ // noop
+ });
+ }, []);
+
+ return {copiedId, handleCopyImport};
+}
+
+function CopyInfoMessage() {
+ return (
+
+
+ Press an item to copy its import statement
+
+ );
+}
+
+interface IconListBoxProps {
+ items: typeof iconList,
+ copiedId: string | null,
+ onAction: (item: string) => void,
+ listBoxClassName?: string
+}
+
+function IconListBox({items, copiedId, onAction, listBoxClassName}: IconListBoxProps) {
+ return (
+
+ onAction(item.toString())}
+ items={items}
+ layout="grid"
+ className={listBoxClassName || style({width: '100%', scrollPaddingY: 4})}
+ dependencies={[copiedId]}
+ renderEmptyState={() => (
+
+
+ No results
+ Try a different search term.
+
+ )}>
+ {item => }
+
+
+ );
+}
+
const itemStyle = style({
...focusRing(),
size: 'full',
@@ -42,45 +123,27 @@ const itemStyle = style({
cursor: 'default'
});
-let handleCopyImport = (id: string) => {
- navigator.clipboard.writeText(`import ${id} from '@react-spectrum/s2/icons/${id}';`).then(() => {
- // noop
- }).catch(() => {
- // noop
- });
-};
-
interface IconSearchViewProps {
filteredItems: typeof iconList
}
export function IconSearchView({filteredItems}: IconSearchViewProps) {
+ let {copiedId, handleCopyImport} = useCopyImport();
+
return (
-
- handleCopyImport(item.toString())}
- items={filteredItems}
- layout="grid"
- className={style({width: '100%', scrollPaddingY: 4})}
- renderEmptyState={() => (
-
-
- No results
- Try a different search term.
-
- )}>
- {item => }
-
-
+ <>
+
+
+ >
);
}
-function IconItem({item}) {
+function IconItem({item, isCopied = false}: {item: typeof iconList[number], isCopied?: boolean}) {
let Icon = item.icon;
let ref = useRef(null);
return (
-
+ {isCopied ? : }
- {item.id}
+ {isCopied ? 'Copied!' : item.id}
);
@@ -163,39 +226,23 @@ export function IconSearchSkeleton() {
);
}
-export function IconCards() {
- let {contains} = useFilter({sensitivity: 'base'});
- let filter = useCallback((textValue, inputValue) => {
- // check if we're typing part of a category alias
- for (const alias of Object.keys(iconAliases)) {
- if (contains(alias, inputValue) && iconAliases[alias].includes(textValue)) {
- return true;
- }
- }
- // also compare for substrings in the icon's actual name
- return textValue != null && contains(textValue, inputValue);
- }, [contains]);
+export function IconsPageSearch() {
+ let filter = useIconFilter();
+ let {copiedId, handleCopyImport} = useCopyImport();
+
return (
-
-
-
-
- handleCopyImport(item.toString())}
+ <>
+
+
+
+
+ (
-
-
- No results
- Try a different search term.
-
- )}>
- {item => }
-
-
-
-
+ copiedId={copiedId}
+ onAction={handleCopyImport}
+ listBoxClassName={style({height: 440, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})} />
+
+
+ >
);
}
diff --git a/packages/dev/s2-docs/src/Layout.tsx b/packages/dev/s2-docs/src/Layout.tsx
index 7608a89d3f1..a4ea38f0230 100644
--- a/packages/dev/s2-docs/src/Layout.tsx
+++ b/packages/dev/s2-docs/src/Layout.tsx
@@ -216,7 +216,7 @@ export function Layout(props: PageProps & {children: ReactElement}) {
})}>
0 ? : null}
+ toc={(currentPage.tableOfContents?.[0]?.children?.length ?? 0) > 1 ? : null}
pages={pages}
currentPage={currentPage} />
diff --git a/packages/dev/s2-docs/src/ReleasesList.tsx b/packages/dev/s2-docs/src/ReleasesList.tsx
index 2f97c78e46b..2874614d850 100644
--- a/packages/dev/s2-docs/src/ReleasesList.tsx
+++ b/packages/dev/s2-docs/src/ReleasesList.tsx
@@ -19,12 +19,7 @@ export function ReleasesList({pages}: {pages: Page[]}) {
{renderHTMLfromMarkdown(release.exports?.description, {})}
))}
-
-
-
For all previous releases or React Spectrum v3, see the Archived releases page.
-
+ For all previous releases or React Spectrum v3, see the Archived releases page.
);
}
diff --git a/packages/dev/s2-docs/src/SearchMenu.tsx b/packages/dev/s2-docs/src/SearchMenu.tsx
index 6e735004f03..54c60e986b7 100644
--- a/packages/dev/s2-docs/src/SearchMenu.tsx
+++ b/packages/dev/s2-docs/src/SearchMenu.tsx
@@ -1,18 +1,17 @@
'use client';
import {ActionButton, Content, Heading, IllustratedMessage, SearchField, Tag, TagGroup} from '@react-spectrum/s2';
-import {Autocomplete, Dialog, Key, OverlayTriggerStateContext, Provider, Separator as RACSeparator, useFilter} from 'react-aria-components';
+import {Autocomplete, Dialog, Key, OverlayTriggerStateContext, Provider, Separator as RACSeparator} from 'react-aria-components';
import Close from '@react-spectrum/s2/icons/Close';
import {ComponentCardView} from './ComponentCardView';
import {getLibraryFromPage, getLibraryFromUrl} from './library';
-import {iconAliases} from './iconAliases.js';
-import {iconList, IconSearchSkeleton} from './IconSearchView';
+import {iconList, IconSearchSkeleton, useIconFilter} from './IconSearchView';
import {type Library, TAB_DEFS} from './constants';
// eslint-disable-next-line monorepo/no-internal-import
import NoSearchResults from '@react-spectrum/s2/illustrations/linear/NoSearchResults';
// @ts-ignore
import {Page} from '@parcel/rsc';
-import React, {CSSProperties, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {CSSProperties, lazy, Suspense, useEffect, useMemo, useRef, useState} from 'react';
import {SelectableCollectionContext} from '../../../react-aria-components/src/RSPContexts';
import {style} from '@react-spectrum/s2/style' with { type: 'macro' };
import {Tab, TabList, TabPanel, Tabs} from './Tabs';
@@ -141,18 +140,7 @@ export function SearchMenu(props: SearchMenuProps) {
const [selectedSectionId, setSelectedSectionId] = useState(() => currentPage.exports?.section?.toLowerCase() || 'components');
const prevSearchWasEmptyRef = useRef(true);
- // Icon filter function
- const {contains} = useFilter({sensitivity: 'base'});
- const iconFilter = useCallback((textValue, inputValue) => {
- // check if we're typing part of a category alias
- for (const alias of Object.keys(iconAliases)) {
- if (contains(alias, inputValue) && iconAliases[alias].includes(textValue)) {
- return true;
- }
- }
- // also compare for substrings in the icon's actual name
- return textValue != null && contains(textValue, inputValue);
- }, [contains]);
+ const iconFilter = useIconFilter();
const filteredIcons = useMemo(() => {
if (!searchValue.trim()) {