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
136 changes: 68 additions & 68 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions projects/cli/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ const MCP_UI_ENTRYPOINTS = ['api-icons-list.html', 'api-tokens-list.html', 'exam
const MCP_UI_INLINE_MODULE_ID = 'virtual:mcp-ui-inline';
const PACKAGE_JSON_PATH = resolve(import.meta.dirname, 'package.json');

export default defineConfig(async ({ command }) => {
if (command === 'build') await buildMcpUiResources();

export default defineConfig(() => {
const libConfig = libraryNodeBuildConfig;
const rollupOptions = libConfig.build?.rolldownOptions;
if (rollupOptions?.external) rollupOptions.external = NODE_BUILT_IN_MODULES;
Expand Down Expand Up @@ -53,6 +51,9 @@ function loadMcpUiResourcesPlugin(): Plugin {
return {
name: 'load-mcp-ui-resources',
enforce: 'pre',
async buildStart() {
await buildMcpUiResources();
},
load(id) {
const htmlPath = id.match(/^(.+\.html)\?raw(?:$|&)/)?.[1];
if (!htmlPath || dirname(htmlPath) !== MCP_UI_SOURCE_DIR) return null;
Expand Down
3 changes: 3 additions & 0 deletions projects/core/.visual/menu-group.dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions projects/core/.visual/menu-group.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions projects/core/src/combobox/combobox.test.lighthouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('combobox lighthouse report', () => {
expect(report.scores.performance).toBe(100);
expect(report.scores.accessibility).toBe(100);
expect(report.scores.bestPractices).toBe(100);
expect(report.payload.javascript.kb).toBeLessThan(36.7);
expect(report.payload.javascript.kb).toBeLessThan(36.9);
});

test('combobox multi select with large dataset should meet lighthouse benchmarks', async () => {
Expand All @@ -42,6 +42,6 @@ describe('combobox lighthouse report', () => {

expect(report.scores.performance).toBeGreaterThanOrEqual(96);
expect(report.scores.bestPractices).toBe(100);
expect(report.payload.javascript.kb).toBeLessThan(36.1);
expect(report.payload.javascript.kb).toBeLessThan(36.2);
});
});
4 changes: 2 additions & 2 deletions projects/core/src/index.test.lighthouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('lighthouse report', () => {
expect(report.scores.performance).toBe(100);
expect(report.scores.accessibility).toBe(100);
expect(report.scores.bestPractices).toBe(100);
expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(122.8);
expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(123.5);

// if sudden drop in size, check vite bundle config and bundle demo to ensure side effects are properly preserved
expect(report.payload.javascript.requests['index.js'].kb).toBeGreaterThan(100);
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('lighthouse report', () => {
expect(report.scores.accessibility).toBe(100);
expect(report.scores.bestPractices).toBe(100);
expect(report.payload.javascript.requests[Object.keys(report.payload.javascript.requests)[0]].kb).toBeLessThan(
103.0
104.0
);
});
});
9 changes: 7 additions & 2 deletions projects/core/src/internal/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,14 @@ export type Prominence = 'emphasis' | 'muted';
* - `inline` Element container reduces to fit within inline content such as a block of text.
* - `inset` Element container optimizes for embedding or inset placement inside another containing element.
* - `full` Element container optimizes for filling its container bounds.
* - `condensed` Element container optimizes for small, summarized or contained spaces.
*/
export type Container = 'inline' | 'flat' | 'inset' | 'full' | 'condensed';
export type Container = 'inline' | 'flat' | 'inset' | 'full';

/** Controls the visual density of an element to match its context and available space.
* - `compact` - Reduces visual weight and spacing for dense layouts or secondary elements with less visual prominence.
* - `default` - Standard size that works well in most contexts and provides balanced visibility.
*/
export type Density = 'compact' | 'default';

/** Communicates the intent and semantic meaning of an element to help users understand the outcome of their actions.
* - `accent` - Highlights important actions or draws attention to primary interactive elements.
Expand Down
3 changes: 2 additions & 1 deletion projects/core/src/internal/utils/audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class AuditTestElement extends LitElement {
};

render() {
return html`<slot></slot>`;
return html`<slot></slot><slot name="suffix"></slot>`;
}
}

Expand Down Expand Up @@ -63,6 +63,7 @@ describe('audit', () => {
<audit-test-element>
<p nve-text="body"></p>
<span></span>
<button slot="suffix">Action</button>
</audit-test-element>
</audit-test-parent-element>`);
element = fixture.querySelector('audit-test-element') as AuditTestElement;
Expand Down
6 changes: 3 additions & 3 deletions projects/core/src/internal/utils/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export function auditSlots(
host: HTMLElement & { constructor: { metadata?: { children?: string[]; disallowedChildren?: string[] } } }
): [Element[], string[]] {
const validElements = ['template', ...(host.constructor.metadata?.children ?? [])].filter(c => typeof c === 'string');
const invalidElements = Array.from(host.shadowRoot!.querySelectorAll('slot')).flatMap(slot =>
slot.assignedElements().filter(e => !validElements.map(i => i).includes(e.localName))
);
const invalidElements = Array.from(host.shadowRoot!.querySelectorAll('slot'))
.filter(slot => slot.name === '')
.flatMap(slot => slot.assignedElements().filter(e => !validElements.map(i => i).includes(e.localName)));
return [invalidElements, validElements];
}

Expand Down
4 changes: 3 additions & 1 deletion projects/core/src/menu/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
// SPDX-License-Identifier: Apache-2.0

import { define } from '@nvidia-elements/core/internal';
import { Menu, MenuItem } from '@nvidia-elements/core/menu';
import { Menu, MenuGroup, MenuItem } from '@nvidia-elements/core/menu';

define(Menu);
define(MenuGroup);
define(MenuItem);

declare global {
interface HTMLElementTagNameMap {
'nve-menu': Menu;
'nve-menu-group': MenuGroup;
'nve-menu-item': MenuItem;
}
}
1 change: 1 addition & 0 deletions projects/core/src/menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
// SPDX-License-Identifier: Apache-2.0

export * from './menu.js';
export * from './menu-group.js';
export * from './menu-item.js';
107 changes: 107 additions & 0 deletions projects/core/src/menu/menu-group.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */
/* SPDX-License-Identifier: Apache-2.0 */

:host {
--background: transparent;
--font-size: var(--nve-ref-font-size-100);
--padding: var(--nve-ref-size-200);
--gap: var(--nve-ref-space-sm);
--border-radius: var(--nve-ref-border-radius-xs);
--color: var(--nve-sys-interaction-color);
--width: 100%;
--min-height: var(--nve-ref-size-800);
--nve-sys-interaction-state-base: var(--background, var(--nve-sys-layer-canvas-background));
display: block;
width: var(--width);
contain: layout;
}

[internal-host] {
display: flex;
flex-direction: column;
width: 100%;
gap: var(--nve-ref-space-xxs);
}

button {
appearance: none;
padding: var(--padding);
color: var(--color);
background: var(--background);
font-size: var(--font-size);
border-radius: var(--border-radius);
min-height: var(--min-height);
gap: var(--gap);
font-weight: var(--nve-ref-font-weight-medium);
border: 0;
width: 100%;
margin: 0;
font-family: inherit;
line-height: normal;
text-align: start;
cursor: pointer;
outline-offset: calc(-1 * var(--nve-ref-size-50));
position: relative;
display: flex;
align-items: center;
background-image: linear-gradient(
color-mix(
in oklab,
var(--nve-sys-interaction-state-base) 100%,
var(--nve-sys-interaction-state-mix) var(--nve-sys-interaction-state-ratio)
)
0 0
) !important;

&:hover {
--nve-sys-interaction-state-ratio: var(--nve-sys-interaction-state-ratio-hover);
}
}

button:not([disabled]):focus-visible {
outline: Highlight solid 2px !important;
outline: 5px auto -webkit-focus-ring-color !important;
}

slot:not([name]) {
display: inline-flex;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

[part='caret'] {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
flex: none;
--color: currentColor;
}

slot[name='menu'] {
width: 100%;
display: block;
}

::slotted(nve-menu) {
--min-width: 0;
--width: 100%;
--padding: 0 0 0 var(--nve-ref-size-200);
}

::slotted(nve-icon) {
--color: currentColor;
}

:host([disabled]) {
--color: var(--nve-sys-interaction-disabled-color);
}

:host([disabled]) button {
--nve-sys-interaction-state-ratio: 0;
cursor: not-allowed;
opacity: var(--nve-ref-opacity-500);
}

:host(:not([disabled]):hover) {
--color: var(--nve-sys-interaction-selected-color);
}
50 changes: 50 additions & 0 deletions projects/core/src/menu/menu-group.test.axe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { html } from 'lit';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
import { runAxe } from '@internals/testing/axe';
import { MenuGroup } from '@nvidia-elements/core/menu';
import '@nvidia-elements/core/dot/define.js';
import '@nvidia-elements/core/icon/define.js';
import '@nvidia-elements/core/menu/define.js';

describe(MenuGroup.metadata.tag, () => {
let fixture: HTMLElement;

beforeEach(async () => {
fixture = await createFixture(html`
<nve-menu-group>
Collapsed
<nve-menu><nve-menu-item>Item</nve-menu-item></nve-menu>
</nve-menu-group>
<nve-menu-group expanded>
<nve-icon name="folder" slot="prefix" aria-hidden="true"></nve-icon>
Expanded
<nve-dot slot="suffix" aria-label="2 projects">2</nve-dot>
<nve-menu>
<nve-menu-item current="page">Current item</nve-menu-item>
<nve-menu-item>Other item</nve-menu-item>
</nve-menu>
</nve-menu-group>
<nve-menu-group expanded disabled>
Disabled
<nve-menu><nve-menu-item>Visible item</nve-menu-item></nve-menu>
</nve-menu-group>
`);

await Promise.all(
Array.from(fixture.querySelectorAll<MenuGroup>(MenuGroup.metadata.tag)).map(element => elementIsStable(element))
);
});

afterEach(() => {
removeFixture(fixture);
});

it('should pass axe check', async () => {
const results = await runAxe([MenuGroup.metadata.tag]);
expect(results.violations.length).toBe(0);
});
});
31 changes: 31 additions & 0 deletions projects/core/src/menu/menu-group.test.lighthouse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, test } from 'vitest';
import { lighthouseRunner } from '@internals/vite';

describe('menu-group lighthouse report', () => {
test('menu-group should meet lighthouse benchmarks', async () => {
const report = await lighthouseRunner.getReport(
'nve-menu-group',
/* html */ `
<nve-menu-group behavior-expand expanded>
Resources
<span slot="suffix">2</span>
<nve-menu>
<nve-menu-item current="page">Documentation</nve-menu-item>
<nve-menu-item>Examples</nve-menu-item>
</nve-menu>
</nve-menu-group>
<script type="module">
import '@nvidia-elements/core/menu/define.js';
</script>
`
);

expect(report.scores.performance).toBe(100);
expect(report.scores.accessibility).toBe(100);
expect(report.scores.bestPractices).toBe(100);
expect(report.payload.javascript.kb).toBeLessThan(24);
});
});
27 changes: 27 additions & 0 deletions projects/core/src/menu/menu-group.test.ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { html } from 'lit';
import { describe, expect, it } from 'vitest';
import { ssrRunner } from '@internals/vite';
import { MenuGroup } from '@nvidia-elements/core/menu';
import '@nvidia-elements/core/menu/define.js';

describe(MenuGroup.metadata.tag, () => {
it('should pass baseline ssr check', async () => {
const result = await ssrRunner.render(html`
<nve-menu-group expanded>
Resources
<nve-menu slot="menu">
<nve-menu-item>Documentation</nve-menu-item>
</nve-menu>
</nve-menu-group>
`);

expect(result.includes('shadowroot="open"')).toBe(true);
expect(result.includes('nve-menu-group')).toBe(true);
expect(result.includes('nve-menu')).toBe(true);
expect(result.includes('slot="menu"')).toBe(true);
expect(result.includes('aria-controls="content"')).toBe(true);
});
});
Loading
Loading