Skip to content
Merged
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: 3 additions & 0 deletions packages/@react-spectrum/s2/src/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export interface ActionBarProps extends SlotProps {

export const ActionBarContext = createContext<ContextValue<Partial<ActionBarProps>, DOMRefValue<HTMLDivElement>>>(null);

/**
* Action bars are used for single and bulk selection patterns when a user needs to perform actions on one or more items at the same time.
*/
export const ActionBar = forwardRef(function ActionBar(props: ActionBarProps, ref: DOMRef<HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, ActionBarContext);
let domRef = useDOMRef(ref);
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-spectrum/s2/src/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ const selectionSpanStyles = style({
forcedColorAdjust: 'none'
});

/**
* Calendars display a grid of days in one or more months and allow users to select a single date.
*/
export const Calendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function Calendar<T extends DateValue>(props: CalendarProps<T>, ref: ForwardedRef<HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, CalendarContext);
let {
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/s2/src/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ export interface UserCardProps extends Omit<CardProps, 'density' | 'variant'> {
variant?: 'primary' | 'secondary' | 'tertiary'
}

export const UserCard = forwardRef(function UserCard(props: CardProps, ref: DOMRef<HTMLDivElement>) {
export const UserCard = forwardRef(function UserCard(props: UserCardProps, ref: DOMRef<HTMLDivElement>) {
let {size = 'M'} = props;
return (
<Card {...props} ref={ref} density="spacious">
Expand Down
4 changes: 4 additions & 0 deletions packages/@react-spectrum/s2/src/DateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ const iconStyles = style({
justifyContent: 'end'
});

/**
* DateFields allow users to enter and edit date and time values using a keyboard.
* Each part of a date value is displayed in an individually editable segment.
*/
export const DateField = /*#__PURE__*/ (forwardRef as forwardRefType)(function DateField<T extends DateValue>(
props: DateFieldProps<T>, ref: Ref<HTMLDivElement>
): ReactElement {
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-spectrum/s2/src/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ export const timeField = style({
width: 'unset'
});

/**
* DatePickers combine a DateField and a Calendar popover to allow users to enter or select a date and time value.
*/
export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function DatePicker<T extends DateValue>(
props: DatePickerProps<T>, ref: Ref<HTMLDivElement>
): ReactElement {
Expand Down
4 changes: 4 additions & 0 deletions packages/@react-spectrum/s2/src/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export interface DateRangePickerProps<T extends DateValue> extends

export const DateRangePickerContext = createContext<ContextValue<Partial<DateRangePickerProps<any>>, HTMLDivElement>>(null);

/**
* DateRangePickers combine two DateFields and a RangeCalendar popover to allow users
* to enter or select a date and time range.
*/
export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function DateRangePicker<T extends DateValue>(
props: DateRangePickerProps<T>, ref: Ref<HTMLDivElement>
): ReactElement {
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-spectrum/s2/src/RangeCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const headerStyles = style({
width: 'full'
});

/**
* RangeCalendars display a grid of days in one or more months and allow users to select a contiguous range of dates.
*/
export const RangeCalendar = /*#__PURE__*/ (forwardRef as forwardRefType)(function RangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, ref: ForwardedRef<HTMLDivElement>) {
[props, ref] = useSpectrumContextProps(props, ref, RangeCalendarContext);
let {
Expand Down
3 changes: 3 additions & 0 deletions packages/@react-spectrum/s2/src/RangeSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export interface RangeSliderProps extends Omit<SliderBaseProps<RangeValue<number

export const RangeSliderContext = createContext<ContextValue<Partial<RangeSliderProps>, FocusableRefValue<HTMLDivElement>>>(null);

/**
* RangeSliders allow users to quickly select a subset range. They should be used when the upper and lower bounds to the range are invariable.
*/
export const RangeSlider = /*#__PURE__*/ forwardRef(function RangeSlider(props: RangeSliderProps, ref: FocusableRef<HTMLDivElement>) {
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2');
[props, ref] = useSpectrumContextProps(props, ref, RangeSliderContext);
Expand Down
4 changes: 4 additions & 0 deletions packages/@react-spectrum/s2/src/TimeField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface TimeFieldProps<T extends TimeValue> extends

export const TimeFieldContext = createContext<ContextValue<Partial<TimeFieldProps<any>>, HTMLDivElement>>(null);

/**
* TimeFields allow users to enter and edit time values using a keyboard.
* Each part of the time is displayed in an individually editable segment.
*/
export const TimeField = /*#__PURE__*/ (forwardRef as forwardRefType)(function TimeField<T extends TimeValue>(
props: TimeFieldProps<T>, ref: Ref<HTMLDivElement>
): ReactElement {
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-spectrum/s2/stories/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const meta: Meta<CardProps & {isLoading?: boolean}> = {

export default meta;
type Story = StoryObj<typeof Card>;
type UserCardStory = StoryObj<typeof UserCard>;

export const Example: Story = {
render: (args) => (
Expand Down Expand Up @@ -124,7 +125,7 @@ export const Asset: Story = {
argTypes: specificArgTypes
};

export const User: Story = {
export const User: UserCardStory = {
render: (args) => (
<div style={{display: 'flex', gap: 16, flexWrap: 'wrap', justifyContent: 'center'}}>
<UserCard {...args}>
Expand Down
127 changes: 127 additions & 0 deletions packages/dev/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# @react-spectrum/mcp

The `@react-spectrum/mcp` package allows you to run [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) servers for React Spectrum (S2) and React Aria locally. It exposes a set of tools that MCP clients can discover and call to browse the docs.

## Using with an MCP client

Add one or both servers to your MCP client configuration (the exact file and schema may depend on your client).

```json
{
"mcpServers": {
"s2-docs": {
"command": "npx",
"args": ["@react-spectrum/mcp", "s2"]
},
"react-aria-docs": {
"command": "npx",
"args": ["@react-spectrum/mcp", "react-aria"]
}
}
}
```

<details>
<summary>Cursor</summary>

#### Click the button to install:

React Spectrum (S2):

[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=s2-docs&config=eyJjb21tYW5kIjoibnB4IEByZWFjdC1zcGVjdHJ1bS9tY3AgczIifQ%3D%3D)

React Aria:

[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=react-aria-docs&config=eyJjb21tYW5kIjoibnB4IEByZWFjdC1zcGVjdHJ1bS9tY3AgcmVhY3QtYXJpYSJ9)

Or follow the MCP install [guide](https://docs.cursor.com/en/context/mcp#installing-mcp-servers) and use the standard config above.

</details>

<details>
<summary>VS Code</summary>

#### Click the button to install:

React Spectrum (S2):

[<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](vscode:mcp/install?%7B%22name%22%3A%22s2-docs%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22%40react-spectrum%2Fmcp%22%2C%22s2%22%5D%7D)

React Aria:

[<img src="https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF" alt="Install in VS Code">](vscode:mcp/install?%7B%22name%22%3A%22react-aria-docs%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22%40react-spectrum%2Fmcp%22%2C%22react-aria%22%5D%7D)


#### Or install manually:

Follow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server) and use the standard config above. You can also add servers using the VS Code CLI:

```bash
# For VS Code
code --add-mcp '{"name":"s2-docs","command":"npx","args":["@react-spectrum/mcp","s2"]}'
code --add-mcp '{"name":"react-aria-docs","command":"npx","args":["@react-spectrum/mcp","react-aria"]}'
```

</details>

<details>
<summary>Claude Code</summary>

Use the Claude Code CLI to add the servers:

```bash
claude mcp add s2-docs npx @react-spectrum/mcp s2
claude mcp add react-aria-docs npx @react-spectrum/mcp react-aria
```
For more information, see the [Claude Code MCP documentation](https://docs.claude.com/en/docs/claude-code/mcp).
</details>

<details>
<summary>Codex</summary>

Create or edit the configuration file `~/.codex/config.toml` and add:

```toml
[mcp_servers.s2-docs]
command = "npx"
args = ["@react-spectrum/mcp", "s2"]

[mcp_servers.react-aria-docs]
command = "npx"
args = ["@react-spectrum/mcp", "react-aria"]
```

For more information, see the [Codex MCP documentation](https://github.com/openai/codex/blob/main/docs/config.md#mcp_servers).

</details>

<details>
<summary>Gemini CLI</summary>

Use the Gemini CLI to add the servers:

```bash
gemini mcp add s2-docs npx @react-spectrum/mcp s2
gemini mcp add react-aria-docs npx @react-spectrum/mcp react-aria
```

For more information, see the [Gemini CLI MCP documentation](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#how-to-set-up-your-mcp-server).

</details>

<details>
<summary>Windsurf</summary>

Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp) and use the standard config above.

</details>

## Tools

| Tool | Input | Description |
| --- | --- | --- |
| `list_pages` | `{ includeDescription?: boolean }` | List available pages in the selected docs library. |
| `get_page_info` | `{ page_name: string }` | Return page description and list of section titles. |
| `get_page` | `{ page_name: string, section_name?: string }` | Return full page markdown, or only the specified section. |
| `search_icons` (S2 only) | `{ terms: string or string[] }` | Search S2 workflow icon names. |
| `search_illustrations` (S2 only) | `{ terms: string or string[] }` | Search S2 illustration names. |
2 changes: 1 addition & 1 deletion packages/dev/mcp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"bin": "dist/index.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"build": "node ./scripts/build-data.mjs && tsc -p tsconfig.json",
"start": "node dist/index.js",
"dev": "node --enable-source-maps dist/index.js"
},
Expand Down
77 changes: 77 additions & 0 deletions packages/dev/mcp/scripts/build-data.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env node
import fg from 'fast-glob';
import {fileURLToPath, pathToFileURL} from 'url';
import fs from 'fs';
import path from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPO_ROOT = path.resolve(__dirname, '../../../..');
const OUT_DIR = path.resolve(__dirname, '../dist/data');

const ICONS_DIR = path.resolve(REPO_ROOT, 'packages/@react-spectrum/s2/s2wf-icons');
const ILLUSTRATIONS_DIR = path.resolve(REPO_ROOT, 'packages/@react-spectrum/s2/spectrum-illustrations/linear');
const ICON_ALIASES_JS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/iconAliases.js');
const ILLUSTRATION_ALIASES_JS = path.resolve(REPO_ROOT, 'packages/dev/s2-docs/src/illustrationAliases.js');

function ensureDir(p) {
fs.mkdirSync(p, {recursive: true});
}

function writeJson(file, data) {
ensureDir(path.dirname(file));
fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
console.log('Wrote', path.relative(REPO_ROOT, file));
}

function buildIconNames() {
if (!fs.existsSync(ICONS_DIR)) {return null;}
const files = fg.sync('*.svg', {cwd: ICONS_DIR, absolute: false, suppressErrors: true});
const ids = Array.from(new Set(
files.map(f => f.replace(/\.svg$/i, '').replace(/^S2_Icon_(.*?)(Size\d+)?_2.*/, '$1'))
)).sort((a, b) => a.localeCompare(b));
return ids;
}

function buildIllustrationNames() {
if (!fs.existsSync(ILLUSTRATIONS_DIR)) {return null;}
const files = fg.sync('**/*.svg', {cwd: ILLUSTRATIONS_DIR, absolute: false, suppressErrors: true});
const ids = Array.from(new Set(
files.map(f => {
const base = f.replace(/\.svg$/i, '').replace(/^S2_lin_(.*)_\d+$/, '$1');
return base ? (base.charAt(0).toUpperCase() + base.slice(1)) : base;
})
)).sort((a, b) => a.localeCompare(b));
return ids;
}

async function loadAliases(modPath, exportName) {
if (!fs.existsSync(modPath)) {return {};}
const mod = await import(pathToFileURL(modPath).href);
return mod[exportName] ?? {};
}

async function main() {
const icons = buildIconNames();
const illustrations = buildIllustrationNames();
const iconAliases = await loadAliases(ICON_ALIASES_JS, 'iconAliases');
const illustrationAliases = await loadAliases(ILLUSTRATION_ALIASES_JS, 'illustrationAliases');

if (icons && icons.length) {
writeJson(path.join(OUT_DIR, 'icons.json'), icons);
}
if (illustrations && illustrations.length) {
writeJson(path.join(OUT_DIR, 'illustrations.json'), illustrations);
}
if (iconAliases && Object.keys(iconAliases).length) {
writeJson(path.join(OUT_DIR, 'iconAliases.json'), iconAliases);
}
if (illustrationAliases && Object.keys(illustrationAliases).length) {
writeJson(path.join(OUT_DIR, 'illustrationAliases.json'), illustrationAliases);
}
}

main().catch((err) => {
console.error(err?.stack || String(err));
process.exit(1);
});
Loading
Loading