Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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 <Link href="llms.txt" target="_blank">lms.txt</Link> which can help AI agents navigate the docs.

## Build a component from scratch

Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/pages/react-aria/useDrop.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ function DropTarget() {
);
}
return (
<div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable grid ${isDropTarget ? 'target' : ''}`}>
<div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable grid ${isDropTarget ? 'target' : ''}`} style={{overflow: 'auto'}}>
{contents}
</div>
);
Expand Down Expand Up @@ -309,7 +309,7 @@ function DropTarget() {
onDrop: onEvent
});
return (
<ul {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} style={{display: 'block', width: 'auto'}}>
<ul {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} style={{display: 'block', width: 'auto', overflow: 'auto'}}>
{events.map((e, i) => <li key={i}>{e}</li>)}
</ul>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/pages/s2/Icons.mdx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -24,7 +24,7 @@ import Edit from "@react-spectrum/s2/icons/Edit";

## Available icons

<IconCards />
<IconsPageSearch />

## API

Expand Down
6 changes: 3 additions & 3 deletions packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
167 changes: 107 additions & 60 deletions packages/dev/s2-docs/src/IconSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
let timeout = useRef<ReturnType<typeof setTimeout> | 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 (
<div className={style({display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4})}>
<InfoCircle styles={iconStyle({size: 'XS'})} />
<span className={style({font: 'ui'})}>Press an item to copy its import statement</span>
</div>
);
}

interface IconListBoxProps {
items: typeof iconList,
copiedId: string | null,
onAction: (item: string) => void,
listBoxClassName?: string
}

function IconListBox({items, copiedId, onAction, listBoxClassName}: IconListBoxProps) {
return (
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
<ListBox
onAction={(item) => onAction(item.toString())}
items={items}
layout="grid"
className={listBoxClassName || style({width: '100%', scrollPaddingY: 4})}
dependencies={[copiedId]}
renderEmptyState={() => (
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
<NoSearchResults />
<Heading>No results</Heading>
<Content>Try a different search term.</Content>
</IllustratedMessage>
)}>
{item => <IconItem item={item} isCopied={copiedId === item.id} />}
</ListBox>
</Virtualizer>
);
}

const itemStyle = style({
...focusRing(),
size: 'full',
Expand Down Expand Up @@ -42,53 +123,35 @@ 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 (
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
<ListBox
onAction={(item) => handleCopyImport(item.toString())}
items={filteredItems}
layout="grid"
className={style({width: '100%', scrollPaddingY: 4})}
renderEmptyState={() => (
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
<NoSearchResults />
<Heading>No results</Heading>
<Content>Try a different search term.</Content>
</IllustratedMessage>
)}>
{item => <IconItem item={item} />}
</ListBox>
</Virtualizer>
<>
<CopyInfoMessage />
<IconListBox items={filteredItems} copiedId={copiedId} onAction={handleCopyImport} />
</>
);
}

function IconItem({item}) {
function IconItem({item, isCopied = false}: {item: typeof iconList[number], isCopied?: boolean}) {
let Icon = item.icon;
let ref = useRef(null);
return (
<ListBoxItem id={item.id} value={item} textValue={item.id} className={itemStyle} ref={ref} style={pressScale(ref)}>
<Icon styles={iconStyle({size: 'XL'})} />
{isCopied ? <CheckmarkCircle styles={iconStyle({size: 'XL'})} /> : <Icon styles={iconStyle({size: 'XL'})} />}
<div
className={style({
maxWidth: '100%',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap'
})}>
{item.id}
{isCopied ? 'Copied!' : item.id}
</div>
</ListBoxItem>
);
Expand Down Expand Up @@ -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 (
<Autocomplete filter={filter}>
<div className={style({display: 'flex', flexDirection: 'column', gap: 8})}>
<SearchField size="L" aria-label="Search icons" placeholder="Search icons" />
<Virtualizer layout={GridLayout} layoutOptions={{minItemSize: new Size(64, 64), maxItemSize: new Size(64, 64), minSpace: new Size(12, 12), preserveAspectRatio: true}}>
<ListBox
onAction={(item) => handleCopyImport(item.toString())}
<>
<Autocomplete filter={filter}>
<div className={style({display: 'flex', flexDirection: 'column', gap: 8})}>
<SearchField size="L" aria-label="Search icons" placeholder="Search icons" />
<CopyInfoMessage />
<IconListBox
items={iconList}
layout="grid"
className={style({height: 440, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})}
renderEmptyState={() => (
<IllustratedMessage styles={style({marginX: 'auto', marginY: 32})}>
<NoSearchResults />
<Heading>No results</Heading>
<Content>Try a different search term.</Content>
</IllustratedMessage>
)}>
{item => <IconItem item={item} />}
</ListBox>
</Virtualizer>
</div>
</Autocomplete>
copiedId={copiedId}
onAction={handleCopyImport}
listBoxClassName={style({height: 440, width: '100%', maxHeight: '100%', overflow: 'auto', scrollPaddingY: 4})} />
</div>
</Autocomplete>
</>
);
}
2 changes: 1 addition & 1 deletion packages/dev/s2-docs/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function Layout(props: PageProps & {children: ReactElement<any>}) {
})}>
<Header pages={pages} currentPage={currentPage} />
<MobileHeader
toc={(currentPage.tableOfContents?.[0]?.children?.length ?? 0) > 0 ? <MobileToc key="toc" toc={currentPage.tableOfContents ?? []} currentPage={currentPage} /> : null}
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'})}>
Expand Down
7 changes: 1 addition & 6 deletions packages/dev/s2-docs/src/ReleasesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ export function ReleasesList({pages}: {pages: Page[]}) {
<p className={style({font: 'body', margin: 0})}>{renderHTMLfromMarkdown(release.exports?.description, {})}</p>
</div>
))}
<div>
<header className={style({marginBottom: 12})}>
<h2 className={style({font: 'heading-lg', margin: 0})}><Link href="https://react-spectrum.adobe.com/releases/index.html">Archived releases</Link></h2>
</header>
<p className={style({font: 'body', margin: 0})}>For all previous releases or React Spectrum v3, see the <Link href="https://react-spectrum.adobe.com/releases/index.html">Archived releases</Link> page.</p>
</div>
<p className={style({font: 'body', margin: 0})}>For all previous releases or React Spectrum v3, see the <Link href="https://react-spectrum.adobe.com/releases/index.html">Archived releases</Link> page.</p>
</article>
);
}
Expand Down
20 changes: 4 additions & 16 deletions packages/dev/s2-docs/src/SearchMenu.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -141,18 +140,7 @@ export function SearchMenu(props: SearchMenuProps) {
const [selectedSectionId, setSelectedSectionId] = useState<string>(() => currentPage.exports?.section?.toLowerCase() || 'components');
const prevSearchWasEmptyRef = useRef<boolean>(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()) {
Expand Down