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
11 changes: 11 additions & 0 deletions projects/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ Install to Cursor with the MCP configuration below.
}
```

### Codex

Install to Codex with the MCP configuration below.

```toml
[mcp_servers.elements]
description = "NVIDIA Elements UI Design System (nve-*), custom element schemas, APIs and examples"
command = "nve"
args = ["mcp"]
```

### Prompts

| Prompt | Description | Example Prompt |
Expand Down
3 changes: 1 addition & 2 deletions projects/core/src/combobox/combobox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,10 @@ describe(Combobox.metadata.tag, () => {
expect(dropdown.matches(':popover-open')).toBe(true);
});

it('should assign trigger and anchor to inner input container', async () => {
it('should assign anchor to inner input container', async () => {
const dropdown = element.shadowRoot.querySelector<Dropdown>(Dropdown.metadata.tag);
const inputContainer = element.shadowRoot.querySelector<HTMLDivElement>('[input]');
expect(dropdown.anchor).toBe(inputContainer);
expect(dropdown.trigger).toBe(inputContainer);
});

it('should hide options on escape keypress', async () => {
Expand Down
18 changes: 9 additions & 9 deletions projects/core/src/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export class Combobox extends Control implements ContainerElement {
const hasNoResults = visibleOptions.filter(o => !o.disabled).length === 0;
const showCreateItem = this.#showCreateItem;
return html`
<nve-dropdown part="dropdown" .popoverType=${'manual'} .modal=${false} @open=${this.#onDropdownOpen} @close=${this.#closeListBox} hidden .anchor=${this.#input as HTMLElement} .trigger=${this.#input as HTMLElement} position="bottom">
<nve-dropdown part="dropdown" .popoverType=${'manual'} .modal=${false} @open=${this.#openDropdown} @close=${this.#closeDropdown} hidden .anchor=${this.#input as HTMLElement} position="bottom">
<nve-menu part="menu" role="listbox" style="--width: 100%; --min-width: fit-content" aria-label=${ifDefined(this.i18n.select)}>
${visibleOptions.map(
o => html`
Expand Down Expand Up @@ -371,8 +371,14 @@ export class Combobox extends Control implements ContainerElement {
}
}

#onDropdownOpen(e: Event) {
(e.target as HTMLElement).hidden = false;
#openDropdown() {
this.#dropdown!.hidden = false;
}

#closeDropdown() {
this.#dropdown!.hidden = true;
this._internals.states.delete('dirty');
this.#validateSingleSelectValue();
}

#setupAutoCompleteKeyEvents() {
Expand Down Expand Up @@ -515,12 +521,6 @@ export class Combobox extends Control implements ContainerElement {
}
}

#closeListBox() {
this.#dropdown!.hidePopover();
this._internals.states.delete('dirty');
this.#validateSingleSelectValue();
}

#validateSingleSelectValue() {
const invalidInputValue =
this.#select &&
Expand Down
6 changes: 6 additions & 0 deletions projects/core/src/select/select.global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */
/* SPDX-License-Identifier: Apache-2.0 */

nve-select select[multiple] option {
pointer-events: none !important;
}
11 changes: 9 additions & 2 deletions projects/core/src/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ import {
I18nController,
onChildListMutation,
getElementUpdate,
scopedRegistry
scopedRegistry,
appendRootNodeStyle
} from '@nvidia-elements/core/internal';
import { Control } from '@nvidia-elements/core/forms';
import { Icon } from '@nvidia-elements/core/icon';
import { Menu, MenuItem } from '@nvidia-elements/core/menu';
import { Dropdown } from '@nvidia-elements/core/dropdown';
import { Tag } from '@nvidia-elements/core/tag';
import styles from './select.css?inline';
import globalStyles from './select.global.css?inline';

/**
* @element nve-select
Expand Down Expand Up @@ -156,7 +158,7 @@ export class Select extends Control {
return this.#select?.size === 0
? html`
<nve-icon name="caret" part="caret" direction="down" size="sm" aria-hidden="true"></nve-icon>
<nve-dropdown part="dropdown" @close=${this.#closeDropdown} @open=${this.#openDropdown} hidden .anchor=${this.#input as HTMLElement} .trigger=${this.#input as HTMLElement} position="bottom">
<nve-dropdown part="dropdown" @close=${this.#closeDropdown} @open=${this.#openDropdown} hidden .anchor=${this.#input as HTMLElement} position="bottom">
${this.#menu}
</nve-dropdown>`
: this.#menu;
Expand Down Expand Up @@ -189,6 +191,11 @@ export class Select extends Control {
});
}

connectedCallback() {
super.connectedCallback();
appendRootNodeStyle(this, globalStyles);
}

disconnectedCallback() {
super.disconnectedCallback();
this.#observers.forEach(observer => observer.disconnect());
Expand Down
89 changes: 89 additions & 0 deletions projects/internals/tools/src/api/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import {
getContextAPIs,
getContextTokens,
getPublishedPackageNames,
searchContextAPIs,
type PartialAPIResult
} from './utils.js';

vi.mock('@internals/metadata', () => ({
ApiService: { search: vi.fn() }
}));

describe('getPublishedPackageNames', () => {
const projects = [
{
Expand Down Expand Up @@ -613,4 +618,88 @@ describe('attributeMetadataToMarkdown', () => {

expect(markdown.includes('| `disabled` | `string` |`true` |')).toBe(true);
});

it('should use the built-in example for nve-layout', () => {
const attribute: Attribute = {
name: 'nve-layout',
description: 'Layout utility attribute',
example: '',
markdown: '',
values: [{ name: 'row' }, { name: 'column' }]
};

const markdown = attributeMetadataToMarkdown(attribute);

expect(markdown).toContain('## nve-layout');
expect(markdown).toContain('nve-layout="row gap:sm"');
expect(markdown).toContain('nve-layout="grid gap:sm span-items:6"');
});

it('should use the built-in example for nve-text', () => {
const attribute: Attribute = {
name: 'nve-text',
description: 'Typography utility attribute',
example: '',
markdown: '',
values: [{ name: 'heading' }, { name: 'body' }]
};

const markdown = attributeMetadataToMarkdown(attribute);

expect(markdown).toContain('## nve-text');
expect(markdown).toContain('nve-text="heading"');
expect(markdown).toContain('nve-text="monospace"');
});
});

describe('searchContextAPIs', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should attach markdown to attribute results that have values', async () => {
const { ApiService } = await import('@internals/metadata');
vi.mocked(ApiService.search).mockResolvedValue([
{ name: 'nve-layout', description: 'Layout utility', values: [{ name: 'row' }], markdown: '' }
] as never);

const results = (await searchContextAPIs('layout')) as Attribute[];

expect(results).toHaveLength(1);
expect(results[0].markdown).toContain('## nve-layout');
});

it('should leave element results without a markdown field untouched', async () => {
const { ApiService } = await import('@internals/metadata');
vi.mocked(ApiService.search).mockResolvedValue([
{ name: 'nve-button', manifest: { metadata: { markdown: 'x' } } }
] as never);

const results = (await searchContextAPIs('button')) as Element[];

expect(results).toHaveLength(1);
expect((results[0] as Attribute).markdown).toBeUndefined();
});

it('should limit results to the configured limit', async () => {
const { ApiService } = await import('@internals/metadata');
vi.mocked(ApiService.search).mockResolvedValue(
Array.from({ length: 5 }, (_, index) => ({ name: `nve-item-${index}` })) as never
);

const results = await searchContextAPIs('item', { limit: 2 });

expect(results).toHaveLength(2);
});

it('should return every result when no limit is provided', async () => {
const { ApiService } = await import('@internals/metadata');
vi.mocked(ApiService.search).mockResolvedValue(
Array.from({ length: 5 }, (_, index) => ({ name: `nve-item-${index}` })) as never
);

const results = await searchContextAPIs('item', {});

expect(results).toHaveLength(5);
});
});
22 changes: 22 additions & 0 deletions projects/internals/tools/src/distill/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ describe('isContextExample', () => {
})
).toBe(false);
});

it('should treat an example with no id, tags, or element as a default', () => {
expect(isContextExample({})).toBe(true);
});
});

describe('rankExample', () => {
Expand All @@ -147,6 +151,10 @@ describe('rankExample', () => {
expect(rankExample({ id: 'button-default' })).toBe(3);
});

it('should default to the lowest rank when the id is missing', () => {
expect(rankExample({})).toBe(3);
});

it('should strip elements- prefix before ranking', () => {
expect(rankExample({ id: 'elements-template-foo' })).toBe(0);
expect(rankExample({ id: 'elements-pattern-form' })).toBe(1);
Expand Down Expand Up @@ -267,4 +275,18 @@ describe('distillExamples', () => {
expect(result).toHaveLength(1);
expect(result[0].summary).toBe('Has summary');
});

it('should default every shaped field when examples omit them', () => {
const result = distillExamples([{}, {}]);

expect(result).toHaveLength(2);
expect(result[0]).toEqual({ id: '', name: '', summary: '', element: '', template: '' });
});

it('should fall back to the description when the summary is missing', () => {
const result = distillExamples([{ id: 'widget', element: 'nve-widget', description: 'Reusable widget' }]);

expect(result).toHaveLength(1);
expect(result[0].summary).toBe('Reusable widget');
});
});
63 changes: 63 additions & 0 deletions projects/internals/tools/src/internal/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,68 @@ describe('internal/node', () => {

expect(result).toEqual([]);
});

it('should ignore PATH entries that are not regular files', async () => {
const { existsSync, statSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(statSync).mockReturnValue({ isFile: () => false } as ReturnType<typeof statSync>);

const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('nve', { envPath: '/a' });

expect(result).toEqual([]);
});

it('should expand the command with explicit PATHEXT extensions on win32', async () => {
const { existsSync, statSync, realpathSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
vi.mocked(realpathSync).mockImplementation(path => path.toString());

const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32', pathExt: '.EXE;.CMD' });

expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.exe'))).toBe(true);
expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.cmd'))).toBe(true);
});

it('should fall back to the PATHEXT environment variable on win32', async () => {
const { existsSync, statSync, realpathSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
vi.mocked(realpathSync).mockImplementation(path => path.toString());
vi.stubEnv('PATHEXT', '.BAT');

const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32' });

expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.bat'))).toBe(true);
vi.unstubAllEnvs();
});
Comment on lines +190 to +202

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider moving environment cleanup to ensure it always executes.

The inline vi.unstubAllEnvs() call at line 201 won't execute if the assertion at line 200 fails, potentially leaving environment pollution for subsequent tests.

🧪 Recommended pattern for guaranteed cleanup

Move the cleanup to a try-finally block or use Vitest's afterEach:

Option 1: try-finally pattern

     it('should fall back to the PATHEXT environment variable on win32', async () => {
       const { existsSync, statSync, realpathSync } = await import('node:fs');
       vi.mocked(existsSync).mockReturnValue(true);
       vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
       vi.mocked(realpathSync).mockImplementation(path => path.toString());
       vi.stubEnv('PATHEXT', '.BAT');
 
-      const { findExecutablesOnPath } = await import('./node.js');
-      const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32' });
-
-      expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.bat'))).toBe(true);
-      vi.unstubAllEnvs();
+      try {
+        const { findExecutablesOnPath } = await import('./node.js');
+        const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32' });
+
+        expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.bat'))).toBe(true);
+      } finally {
+        vi.unstubAllEnvs();
+      }
     });

Option 2: Scoped afterEach (if multiple tests need the same cleanup)

describe('win32 PATHEXT tests', () => {
  afterEach(() => {
    vi.unstubAllEnvs();
  });

  it('should fall back to the PATHEXT environment variable on win32', async () => {
    // ... test without inline cleanup
  });
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should fall back to the PATHEXT environment variable on win32', async () => {
const { existsSync, statSync, realpathSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
vi.mocked(realpathSync).mockImplementation(path => path.toString());
vi.stubEnv('PATHEXT', '.BAT');
const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32' });
expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.bat'))).toBe(true);
vi.unstubAllEnvs();
});
it('should fall back to the PATHEXT environment variable on win32', async () => {
const { existsSync, statSync, realpathSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
vi.mocked(realpathSync).mockImplementation(path => path.toString());
vi.stubEnv('PATHEXT', '.BAT');
try {
const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32' });
expect(result.some(commandPath => commandPath.toLowerCase().endsWith('nve.bat'))).toBe(true);
} finally {
vi.unstubAllEnvs();
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@projects/internals/tools/src/internal/node.test.ts` around lines 190 - 202,
Wrap the PATHEXT test so vi.unstubAllEnvs() always runs: in the test that
imports findExecutablesOnPath and stubs PATHEXT, either wrap the test body in a
try-finally where vi.unstubAllEnvs() is called in finally, or move the cleanup
to an afterEach scoped to this describe block (ensuring vi.stubEnv('PATHEXT',
...) remains in the test). This ensures environment cleanup via
vi.unstubAllEnvs() even if the assertion on result.some(...) fails.


it('should treat any matching file as executable on win32 without an access check', async () => {
const { accessSync, existsSync, statSync, realpathSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(statSync).mockReturnValue({ isFile: () => true } as ReturnType<typeof statSync>);
vi.mocked(realpathSync).mockImplementation(path => path.toString());

const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('nve', { envPath: 'C:/bin', platform: 'win32', pathExt: '.EXE' });

expect(result.length).toBeGreaterThan(0);
expect(accessSync).not.toHaveBeenCalled();
});

it('should default to process PATH and platform when options are omitted', async () => {
const { existsSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(false);
vi.stubEnv('PATH', '/usr/bin:/bin');

const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('definitely-not-a-real-command');

expect(result).toEqual([]);
vi.unstubAllEnvs();
});
Comment on lines +217 to +227

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider moving environment cleanup to ensure it always executes.

The inline vi.unstubAllEnvs() call at line 226 won't execute if the assertion at line 225 fails, potentially leaving environment pollution for subsequent tests. This is the same pattern as the test at lines 190-202.

🧪 Recommended pattern for guaranteed cleanup

Use a try-finally block:

     it('should default to process PATH and platform when options are omitted', async () => {
       const { existsSync } = await import('node:fs');
       vi.mocked(existsSync).mockReturnValue(false);
       vi.stubEnv('PATH', '/usr/bin:/bin');
 
-      const { findExecutablesOnPath } = await import('./node.js');
-      const result = findExecutablesOnPath('definitely-not-a-real-command');
-
-      expect(result).toEqual([]);
-      vi.unstubAllEnvs();
+      try {
+        const { findExecutablesOnPath } = await import('./node.js');
+        const result = findExecutablesOnPath('definitely-not-a-real-command');
+
+        expect(result).toEqual([]);
+      } finally {
+        vi.unstubAllEnvs();
+      }
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should default to process PATH and platform when options are omitted', async () => {
const { existsSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(false);
vi.stubEnv('PATH', '/usr/bin:/bin');
const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('definitely-not-a-real-command');
expect(result).toEqual([]);
vi.unstubAllEnvs();
});
it('should default to process PATH and platform when options are omitted', async () => {
const { existsSync } = await import('node:fs');
vi.mocked(existsSync).mockReturnValue(false);
vi.stubEnv('PATH', '/usr/bin:/bin');
try {
const { findExecutablesOnPath } = await import('./node.js');
const result = findExecutablesOnPath('definitely-not-a-real-command');
expect(result).toEqual([]);
} finally {
vi.unstubAllEnvs();
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@projects/internals/tools/src/internal/node.test.ts` around lines 217 - 227,
The test calls vi.stubEnv('PATH', ...) but calls vi.unstubAllEnvs() inline,
which won't run if the assertion fails; update the test for
findExecutablesOnPath so environment restoration always runs (e.g., wrap the
test body in try { ... } finally { vi.unstubAllEnvs(); } or use an afterEach to
call vi.unstubAllEnvs()) ensuring the stubbed PATH is always cleared even on
failures; reference the test that imports and calls findExecutablesOnPath and
the use of vi.stubEnv/vi.unstubAllEnvs.

});
});
33 changes: 31 additions & 2 deletions projects/internals/tools/src/internal/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { ProjectElement } from '@internals/metadata';
import { getElementImports, getAvailableElementTags, wrapText } from './utils.js';
import { getElementImports, getAvailableElementTags, isDebug, wrapText } from './utils.js';

describe('getElementImports', () => {
const elements: ProjectElement[] = [
Expand Down Expand Up @@ -124,4 +124,33 @@ describe('wrapText', () => {
const result = wrapText(text, 10);
expect(result).toContain('superlongwordthatexceedswidth');
});

it('should handle a single word longer than width with nothing trailing', () => {
const text = 'superlongwordthatexceedswidth';
expect(wrapText(text, 10)).toBe('superlongwordthatexceedswidth');
});
});

describe('isDebug', () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it('should return true when debug is enabled outside of the mcp environment', () => {
vi.stubEnv('ELEMENTS_DEBUG', 'true');
vi.stubEnv('ELEMENTS_ENV', 'cli');
expect(isDebug()).toBe(true);
});

it('should return false when debug is not enabled', () => {
vi.stubEnv('ELEMENTS_DEBUG', 'false');
vi.stubEnv('ELEMENTS_ENV', 'cli');
expect(isDebug()).toBe(false);
});

it('should return false in the mcp environment even when debug is enabled', () => {
vi.stubEnv('ELEMENTS_DEBUG', 'true');
vi.stubEnv('ELEMENTS_ENV', 'mcp');
expect(isDebug()).toBe(false);
});
});
Loading