From 43896e5df4c7ce345a934cc3b080e74c9012d6b6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 26 Sep 2026 13:42:00 +0000 Subject: [PATCH] chore(docs): correct generated descriptions, mcp context, and core install Keep authored page descriptions intact and stop truncation from adding a period after an arbitrary cutoff. Give button overview, api, and examples pages distinct descriptions. Point llms.txt at a dedicated mcp context page, and document public npm installation plus react, vue, and angular integration targets for the core package. Signed-off-by: Cursor Agent --- projects/core/README.md | 12 +- projects/core/package.json | 5 +- projects/site/src/_11ty/context/mcp.md | 95 +++++++++++++ projects/site/src/_11ty/layouts/metadata.js | 95 +++++++++++-- .../site/src/_11ty/layouts/metadata.test.ts | 132 ++++++++++++++++-- projects/site/src/_11ty/plugins/llms-txt.js | 5 +- .../site/src/_11ty/plugins/llms-txt.test.ts | 17 ++- projects/site/src/docs/elements/button.md | 1 + 8 files changed, 331 insertions(+), 31 deletions(-) create mode 100644 projects/site/src/_11ty/context/mcp.md diff --git a/projects/core/README.md b/projects/core/README.md index 87c7f292b2..4ca258b833 100644 --- a/projects/core/README.md +++ b/projects/core/README.md @@ -9,16 +9,16 @@ NVIDIA Design System and UI Agent Harness for AI/ML Factories, Robotics, and Aut ## Getting Started -```shell -# local .npmrc file -registry=https://registry.npmjs.org - -# https://registry.npmjs.org -npm login +Install the packages from the public npm registry: +```shell npm install @nvidia-elements/themes @nvidia-elements/styles @nvidia-elements/core ``` +## Framework integration + +Use React, Vue, and Angular as integration targets for these Web Components. See the [integration guides](https://NVIDIA.github.io/elements/docs/integrations/) for framework setup. + Elements ships as many small packages. This allows you to choose what tools your application needs and omit anything unnecessary, improving application performance. ## Usage diff --git a/projects/core/package.json b/projects/core/package.json index cf9b39e545..9cb6621b71 100644 --- a/projects/core/package.json +++ b/projects/core/package.json @@ -12,7 +12,10 @@ "design-system", "ui-components", "accessibility", - "lit" + "lit", + "react", + "vue", + "angular" ], "homepage": "https://NVIDIA.github.io/elements/", "repository": { diff --git a/projects/site/src/_11ty/context/mcp.md b/projects/site/src/_11ty/context/mcp.md new file mode 100644 index 0000000000..a159397af4 --- /dev/null +++ b/projects/site/src/_11ty/context/mcp.md @@ -0,0 +1,95 @@ +# MCP + +The NVIDIA Elements MCP server gives AI assistants Model Context Protocol access to component APIs, examples, design tokens, icons, and project setup. + +## Quick setup + +Install the Elements CLI, then run project setup. The command detects the package manager, configures the MCP server for Cursor, Claude Code, and Codex, and adds the Elements core dependencies. + +```shell +nve project.setup +``` + +## Cursor + +Save this configuration as `.cursor/mcp.json`, then enable the Elements server in Cursor settings. + +```json +{ + "mcpServers": { + "elements": { + "description": "NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples", + "command": "nve", + "args": ["mcp"] + } + } +} +``` + +## Claude Code + +Save this configuration as `.mcp.json`, then restart Claude Code. + +```json +{ + "mcpServers": { + "elements": { + "description": "NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples", + "command": "nve", + "args": ["mcp"] + } + } +} +``` + +## Codex + +Save this configuration as `.codex/config.toml`, then restart Codex. + +```toml +[mcp_servers.elements] +description = "NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples" +command = "nve" +args = ["mcp"] +``` + +## Prompt examples + +- `/artifact` creates a standalone Elements UI artifact. Example prompt: `/artifact Create an example login form`. +- `/doctor` checks Elements setup and MCP configuration. +- `/create-project` creates an Elements starter project. Example prompt: `/create-project Create an Angular todo app`. +- `/migrate` updates a project from deprecated Elements APIs. Example prompt: `/migrate Migrate this project from deprecated Elements APIs`. + +## Tool examples + +`api_validate` checks HTML or JSON. This call checks a button template: + +```json +{ "template": "Save", "format": "json" } +``` + +`examples_get` returns a stored example template. `examples_render` renders custom Elements HTML in the MCP Apps preview. + +| Tool | Description | +| ------------------------- | -------------------------------------------------------- | +| `api_list` | List Elements APIs and components. | +| `api_get` | Read documentation for known components or attributes. | +| `api_validate` | Check HTML and JSON with Elements lint rules. | +| `api_imports_get` | Return ESM imports for an HTML template. | +| `api_tokens_list` | List semantic CSS custom properties and design tokens. | +| `api_icons_list` | List icon names for `nve-icon` and `nve-icon-button`. | +| `examples_list` | List patterns and examples. | +| `examples_get` | Return a known example or pattern template. | +| `examples_render` | Render a custom Elements HTML template in the preview. | +| `playground_validate` | Validate HTML for a playground example. | +| `playground_create` | Create a shareable playground URL from an HTML template. | +| `packages_list` | List the latest published Elements package versions. | +| `packages_get` | Read details for one Elements package. | +| `packages_changelogs_get` | Read changelog details for a package. | +| `project_create` | Create a starter project. | +| `project_validate` | Check project configuration and dependencies. | +| `project_setup` | Set up or update a project to use Elements. | + +## MCP Apps + +Hosts that support MCP Apps can render the example preview, icon list, and token explorer inline. The example preview uses `ui://elements/example-preview` for `examples_get` and `examples_render`. diff --git a/projects/site/src/_11ty/layouts/metadata.js b/projects/site/src/_11ty/layouts/metadata.js index b3f3f9fa78..eefa3cf281 100644 --- a/projects/site/src/_11ty/layouts/metadata.js +++ b/projects/site/src/_11ty/layouts/metadata.js @@ -331,7 +331,8 @@ function getDescriptionContexts(data) { ]; } -function expandShortDescription(data, description) { +function expandShortDescription(data, description, { preserve = false } = {}) { + if (preserve && description.length >= MIN_DESCRIPTION_LENGTH) return description; if (description.length >= MIN_DESCRIPTION_LENGTH) return clampDescription(description); const contexts = getDescriptionContexts(data); @@ -349,16 +350,88 @@ function expandShortDescription(data, description) { return clampDescription(expandedDescription); } +const API_DESCRIPTION_PHRASES = [ + 'Documents properties, events, slots, and CSS.', + 'Includes commands and styling hooks.', + 'Supports production interface workflows.', + 'For Web Components.', + 'For app teams.', + 'For apps.' +]; + +const EXAMPLES_DESCRIPTION_PHRASES = [ + 'Shows markup patterns, states, and usage.', + 'Includes runnable component examples.', + 'Covers actions and layout.', + 'For Web Components.', + 'For app teams.', + 'For apps.' +]; + +function lastIndexBefore(description, max, isMatch, includeChar) { + const limit = Math.min(description.length, max); + let index = -1; + + for (let i = 0; i < limit; i++) { + if (!isMatch(description[i], description[i + 1])) continue; + index = includeChar ? i + 1 : i; + } + + return index; +} + function clampDescription(description) { if (description.length <= MAX_DESCRIPTION_LENGTH) return description; + const sentenceEnd = lastIndexBefore( + description, + MAX_DESCRIPTION_LENGTH, + (char, next) => (char === '.' || char === '!' || char === '?') && (next === undefined || /\s/u.test(next)), + true + ); + if (sentenceEnd >= MIN_DESCRIPTION_LENGTH) return description.slice(0, sentenceEnd).trimEnd(); + + const clauseEnd = lastIndexBefore( + description, + MAX_DESCRIPTION_LENGTH, + char => char === ',' || char === ';' || char === ':' || char === '—', + false + ); + if (clauseEnd >= MIN_DESCRIPTION_LENGTH) { + const clause = description + .slice(0, clauseEnd) + .replace(/[,:;—]+$/u, '') + .trimEnd(); + if (clause.length >= MIN_DESCRIPTION_LENGTH) return clause; + } + const trimmed = description.slice(0, MAX_DESCRIPTION_LENGTH).trimEnd(); const lastSpace = trimmed.lastIndexOf(' '); - const sentence = (lastSpace === -1 ? trimmed : trimmed.slice(0, lastSpace)) - .replace(/[,:;—-]+$/u, '') - .replace(/[.?!]$/u, ''); + return (lastSpace === -1 ? trimmed : trimmed.slice(0, lastSpace)).replace(/[,:;—-]+$/u, '').trimEnd(); +} + +function fitDescription(lead, phrases) { + let text = lead; + + for (const phrase of phrases) { + if (text.length >= MIN_DESCRIPTION_LENGTH) return text; + const next = `${text} ${phrase}`; + if (next.length <= MAX_DESCRIPTION_LENGTH) text = next; + } + + return text; +} + +function componentTabDescription(kind, tag, componentDescription) { + const purpose = kind === 'api' ? `API reference for <${tag}>.` : `Interactive examples for <${tag}>.`; + const qualified = `${componentDescription} — ${purpose}`; + if (qualified.length <= MAX_DESCRIPTION_LENGTH) return qualified; + + const phrases = kind === 'api' ? API_DESCRIPTION_PHRASES : EXAMPLES_DESCRIPTION_PHRASES; + const fitted = fitDescription(purpose, phrases); + if (fitted.length <= MAX_DESCRIPTION_LENGTH) return fitted; - return sentence.length < MAX_DESCRIPTION_LENGTH ? `${sentence}.` : sentence; + return clampDescription(fitted); } function hasGeneratedPage(data, generatedUrls, url) { @@ -375,12 +448,14 @@ export function resolvePageMeta(data) { const componentDescription = element?.manifest?.description?.trim(); let description; - if (data.description) { - description = data.description; + let authored = false; + if (typeof data.description === 'string' && data.description.trim()) { + description = data.description.trim(); + authored = true; } else if (data.isApiTab && componentDescription) { - description = `${componentDescription} — API reference for <${tag}>.`; + description = componentTabDescription('api', tag, componentDescription); } else if (data.isExamplesTab && componentDescription) { - description = `${componentDescription} — Interactive examples for <${tag}>.`; + description = componentTabDescription('examples', tag, componentDescription); } else if (data.changelog?.description) { description = `Changelog for ${data.changelog.title}: ${data.changelog.description}`; } else if (componentDescription) { @@ -392,7 +467,7 @@ export function resolvePageMeta(data) { description = `Documentation for ${rawTitle} in NVIDIA Elements, the framework-agnostic design system for AI/ML factories.`; } - description = expandShortDescription(data, description); + description = expandShortDescription(data, description, { preserve: authored }); const canonicalUrl = getSiteUrl(url); const ogImage = SOCIAL_IMAGE_URL; diff --git a/projects/site/src/_11ty/layouts/metadata.test.ts b/projects/site/src/_11ty/layouts/metadata.test.ts index deeeba6c08..30f3409658 100644 --- a/projects/site/src/_11ty/layouts/metadata.test.ts +++ b/projects/site/src/_11ty/layouts/metadata.test.ts @@ -17,7 +17,35 @@ vi.mock('../../index.11tydata.js', () => ({ name: 'nve-button', version: '1.2.3', manifest: { - tagName: 'nve-button' + tagName: 'nve-button', + description: + 'A button is a widget that enables users to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation.' + } + }, + { + name: 'nve-cutoff', + version: '1.2.3', + manifest: { + tagName: 'nve-cutoff', + description: `${'metadata '.repeat(14).trimEnd()} unbrokenmetadatadescriptionsegmentthatwouldotherwisebecutmidword` + } + }, + { + name: 'nve-sentence', + version: '1.2.3', + manifest: { + tagName: 'nve-sentence', + description: + 'Generated metadata description that already forms a complete sentence about NVIDIA Elements Web Components, production UI, and published documentation. Additional words continue past the limit and should not receive a synthetic period.' + } + }, + { + name: 'nve-homepage', + version: '1.2.3', + manifest: { + tagName: 'nve-homepage', + description: + 'NVIDIA Elements Design System: framework-agnostic Web Components, design tokens, CLI, MCP, skills, and lint tooling for AI infrastructure, robotics, and autonomous vehicle UI.' } }, { @@ -36,6 +64,7 @@ const { AUTHOR_ID, AUTHOR_NAME, AUTHOR_URL, + escapeAttr, renderJsonLd, resolvePageMeta, SOFTWARE_ID, @@ -344,26 +373,105 @@ describe('resolvePageMeta', () => { expect(meta.description).toBe(description); }); - it('should clamp overlong descriptions', () => { + it('should preserve explicitly authored descriptions', () => { + const description = + 'NVIDIA Elements Design System: framework-agnostic Web Components, design tokens, CLI, MCP, skills, and lint tooling for AI infrastructure, robotics, and autonomous vehicle UI.'; const meta = resolvePageMeta({ - page: { url: '/docs/integrations/react/' }, - title: 'React', - description: - 'A complete NVIDIA Elements guide for Web Component integration, API design, accessibility, examples, and production interface implementation across applications.' + page: { url: '/' }, + title: 'NVIDIA Design System for AI UI', + description }); - expect(meta.description.length).toBeLessThanOrEqual(MAX_DESCRIPTION_LENGTH); + expect(meta.description).toBe(description); + expect(meta.description.endsWith('autonomous vehicle UI.')).toBe(true); + expect(meta.description.endsWith('robotics, and.')).toBe(false); }); - it('should clamp overlong descriptions to the available word boundary', () => { + it('should truncate generated descriptions without adding a period', () => { const prefix = 'metadata '.repeat(14).trimEnd(); const meta = resolvePageMeta({ - page: { url: '/docs/integrations/react/' }, - title: 'React', - description: `${prefix} unbrokenmetadatadescriptionsegmentthatwouldotherwisebecutmidword` + page: { url: '/docs/elements/cutoff/' }, + title: 'Cutoff', + tag: 'nve-cutoff' + }); + const sentence = resolvePageMeta({ + page: { url: '/docs/elements/sentence/' }, + title: 'Sentence', + tag: 'nve-sentence' + }); + const homepage = resolvePageMeta({ + page: { url: '/docs/elements/homepage/' }, + title: 'Homepage', + tag: 'nve-homepage' + }); + + expect(meta.description).toBe(prefix); + expect(meta.description.endsWith('.')).toBe(false); + expect(sentence.description).toBe( + 'Generated metadata description that already forms a complete sentence about NVIDIA Elements Web Components, production UI, and published documentation.' + ); + expect(homepage.description.endsWith('and')).toBe(true); + expect(homepage.description.endsWith('and.')).toBe(false); + expect(homepage.description.endsWith('.')).toBe(false); + }); + + it('should publish distinct button overview, api, and examples descriptions', () => { + const overviewDescription = + 'Use the NVIDIA Elements button to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or deleting content in an app.'; + const pages = [ + { + data: { + page: { url: '/docs/elements/button/' }, + collections: { all: [] }, + title: 'Button', + tag: 'nve-button', + description: overviewDescription + }, + description: overviewDescription + }, + { + data: { + page: { url: '/docs/elements/button/api/' }, + collections: { all: [] }, + title: 'Button', + tag: 'nve-button', + isApiTab: true + }, + description: + 'API reference for . Documents properties, events, slots, and CSS. Includes commands and styling hooks. Supports production interface workflows.' + }, + { + data: { + page: { url: '/docs/elements/button/examples/' }, + collections: { all: [] }, + title: 'Button', + tag: 'nve-button', + isExamplesTab: true + }, + description: + 'Interactive examples for . Shows markup patterns, states, and usage. Includes runnable component examples. Covers actions and layout. For app teams.' + } + ]; + + const descriptions = pages.map(({ data, description }) => { + const meta = resolvePageMeta(data); + const html = renderBaseHead(data); + const graph = getGraphJsonLd(html); + + expect(meta.description).toBe(description); + expect(meta.description.length).toBeGreaterThanOrEqual(150); + expect(meta.description.length).toBeLessThanOrEqual(MAX_DESCRIPTION_LENGTH); + expect(meta.description.endsWith('performing a.')).toBe(false); + expect(html).toContain(``); + expect(html).toContain(``); + expect(graph.some(node => node.description === description)).toBe(true); + + return description; }); - expect(meta.description).toBe(`${prefix}.`); + expect(new Set(descriptions).size).toBe(3); + expect(descriptions[1]).toContain('API reference'); + expect(descriptions[2]).toContain('Interactive examples'); }); }); diff --git a/projects/site/src/_11ty/plugins/llms-txt.js b/projects/site/src/_11ty/plugins/llms-txt.js index 283bbb6fe1..e8cf6577e1 100644 --- a/projects/site/src/_11ty/plugins/llms-txt.js +++ b/projects/site/src/_11ty/plugins/llms-txt.js @@ -64,7 +64,7 @@ NVIDIA Elements is NVIDIA's design system for building agent-ready AI infrastruc Use NVIDIA Elements for agentic UI, AI infrastructure dashboards, robotics consoles, autonomous vehicle tools, and internal developer workflows. Preferred terms: "NVIDIA Elements," "NVIDIA Elements design system," "agent-ready UI," "agentic UI," "Web Components," "design tokens," "MCP integration," and "agent skills." Reference APIs as \`nve-*\` custom elements. Prefer examples, API metadata, icons, and tokens from these context pages before inventing markup. - [CLI](${base}/context/cli.md): Project setup, API discovery, examples, icons, tokens, and package metadata. -- [MCP](${base}/context/cli.md): Model Context Protocol integration for AI assistants. +- [MCP](${base}/context/mcp.md): Model Context Protocol integration for AI assistants. - [Skills](${base}/context/skills/index.md): The Elements agent skill and its workflow references. - [CDN](${base}/context/integrations/cdn.md): CDN integration for demos and agent generated artifacts. - [APIs](${base}/context/api/index.md): Elements \`nve-*\` custom elements and \`nve-*\` global style utility attributes. @@ -117,7 +117,9 @@ async function writeLlmsTxtFiles(publicOutputPath) { const cliReadme = await fsp.readFile('../cli/README.md', 'utf-8'); const lintReadme = await fsp.readFile('../lint/README.md', 'utf-8'); + const mcpContext = await fsp.readFile(new URL('../context/mcp.md', import.meta.url), 'utf-8'); await writeContextDoc(nodePath.join(publicOutputPath, 'context', 'cli'), cliReadme); + await writeContextDoc(nodePath.join(publicOutputPath, 'context', 'mcp'), mcpContext); await writeContextDoc(nodePath.join(publicOutputPath, 'context', 'lint'), lintReadme); const cdnReadme = await fsp.readFile('./src/docs/integrations/cdn.md', 'utf-8'); @@ -170,6 +172,7 @@ ${attributes.map(e => `- [${e.name.replace(/^nve-/, '')} (attribute utility)](${ const fullContent = [ content, cliReadme, + mcpContext, lintReadme, skillsContent, ...skillMarkdown, diff --git a/projects/site/src/_11ty/plugins/llms-txt.test.ts b/projects/site/src/_11ty/plugins/llms-txt.test.ts index 9830857640..95df197d45 100644 --- a/projects/site/src/_11ty/plugins/llms-txt.test.ts +++ b/projects/site/src/_11ty/plugins/llms-txt.test.ts @@ -1,3 +1,4 @@ +import { readFile } from 'node:fs/promises'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { createLlmsTxtContent, llmsTxtPlugin } from './llms-txt.js'; import { getPublicOutputPath } from '../utils/public-output.js'; @@ -28,7 +29,8 @@ describe('createLlmsTxtContent', () => { const content = createLlmsTxtContent('https://nvidia.github.io/elements'); expect(content).toContain('[CLI](https://nvidia.github.io/elements/context/cli.md)'); - expect(content).toContain('[MCP](https://nvidia.github.io/elements/context/cli.md)'); + expect(content).toContain('[MCP](https://nvidia.github.io/elements/context/mcp.md)'); + expect(content).not.toContain('[MCP](https://nvidia.github.io/elements/context/cli.md)'); expect(content).toContain('[Skills](https://nvidia.github.io/elements/context/skills/index.md)'); expect(content).toContain('[CDN](https://nvidia.github.io/elements/context/integrations/cdn.md)'); expect(content).toContain('[APIs](https://nvidia.github.io/elements/context/api/index.md)'); @@ -38,6 +40,19 @@ describe('createLlmsTxtContent', () => { expect(content).toContain('[llms-full.txt](https://nvidia.github.io/elements/llms-full.txt)'); }); + it('should point MCP at a dedicated context page', async () => { + const content = createLlmsTxtContent('https://nvidia.github.io/elements'); + const mcpLink = content.match(/\[MCP\]\(([^)]+)\)/)?.[1]; + const source = await readFile(new URL('../context/mcp.md', import.meta.url), 'utf8'); + + expect(mcpLink).toBe('https://nvidia.github.io/elements/context/mcp.md'); + expect(content).toContain('[CLI](https://nvidia.github.io/elements/context/cli.md)'); + expect(source.startsWith('# MCP\n')).toBe(true); + expect(source).toContain('Save'); + expect(source).toContain('/artifact Create an example login form'); + expect(source).toContain('examples_render'); + }); + it('should include preferred terms for AI assistants', () => { const content = createLlmsTxtContent(); diff --git a/projects/site/src/docs/elements/button.md b/projects/site/src/docs/elements/button.md index c18aa83cfb..19a42df6cc 100644 --- a/projects/site/src/docs/elements/button.md +++ b/projects/site/src/docs/elements/button.md @@ -1,6 +1,7 @@ --- { title: 'Button', + description: 'Use the NVIDIA Elements button to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or deleting content in an app.', layout: 'docs.11ty.js', tag: 'nve-button' }