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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { findHtmlTemplate, getTemplateText, hasTag } from './example-helpers.js';

const APPROVED_DOMAINS = ['nvidia.com', 'github.com'];
const APPROVED_DOMAINS = ['nvidia.com', 'github.com', 'huggingface.co'];
const URL_ATTRIBUTES = ['href', 'src', 'srcset'];
const URL_ATTRIBUTE_PATTERN = new RegExp(`\\b(${URL_ATTRIBUTES.join('|')})\\s*=\\s*("([^"]*)"|'([^']*)')`, 'gi');

Expand Down
2 changes: 2 additions & 0 deletions projects/lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ NVIDIA Design System and UI Agent Harness for AI/ML Factories, Robotics, and Aut

The `@nvidia-elements/lint` package is a utility library that provides Elements-specific lint rules to enforce best practices and prevent common errors when using Elements.

The HTML configuration checks HTML in `src/**/*.html`, supported JavaScript and TypeScript templates, and Markdown files under `src/**/*.md`. Markdown linting includes rendered markup and HTML examples in fenced code blocks.

## Getting Started

```shell
Expand Down
2 changes: 1 addition & 1 deletion projects/lint/src/eslint/configs/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import noUnstyledTypography from '../rules/no-unstyled-typography.js';
import noTailwindClasses from '../rules/no-tailwind-classes.js';
import preferAriaLabelInCompactContainers from '../rules/prefer-aria-label-in-compact-containers.js';

const source = ['src/**/*.html', 'src/**/*.js', 'src/**/*.ts', 'src/**/*.tsx'];
const source = ['src/**/*.html', 'src/**/*.js', 'src/**/*.md', 'src/**/*.ts', 'src/**/*.tsx'];

const ignores = [
'node_modules/',
Expand Down
34 changes: 33 additions & 1 deletion projects/lint/src/eslint/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { ESLint } from 'eslint';
import { describe, expect, it } from 'vitest';
import { VERSION } from './index.js';
import { elementsHtmlConfig, VERSION } from './index.js';

describe('VERSION', () => {
it('should export a VERSION const', () => {
expect(VERSION).toBe('0.0.0');
});
});

describe('elementsHtmlConfig', () => {
it('should lint rendered and fenced HTML in Markdown files', async () => {
const eslint = new ESLint({
overrideConfigFile: true,
overrideConfig: [elementsHtmlConfig]
});
const markdown = `---
title: Example
---

<nve-invalid-rendered></nve-invalid-rendered>

\`\`\`html
<nve-invalid-fenced></nve-invalid-fenced>
\`\`\``;

const [result] = await eslint.lintText(markdown, { filePath: 'src/example.md' });

expect(result.messages).toEqual([
expect.objectContaining({
ruleId: '@nvidia-elements/lint/no-unknown-tags',
line: 5
}),
expect.objectContaining({
ruleId: '@nvidia-elements/lint/no-unknown-tags',
line: 8
})
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,51 @@ describe('noExcessivePrimaryActions', () => {
});
});

it('should ignore emphasis buttons inside popover elements', () => {
tester.run('popover emphasis buttons', rule, {
valid: [
`<nve-button interaction="emphasis">Page action one</nve-button>
<nve-dialog>
<nve-dialog-footer>
<nve-button interaction="emphasis">Dialog action one</nve-button>
</nve-dialog-footer>
</nve-dialog>
<nve-dialog>
<nve-dialog-footer>
<nve-button interaction="emphasis">Dialog action two</nve-button>
</nve-dialog-footer>
</nve-dialog>
<nve-button interaction="emphasis">Page action two</nve-button>`,
`<nve-drawer><nve-button interaction="emphasis">Drawer action</nve-button></nve-drawer>
<nve-dropdown><nve-button interaction="emphasis">Dropdown action</nve-button></nve-dropdown>
<nve-notification><nve-button interaction="emphasis">Notification action</nve-button></nve-notification>
<nve-notification-group><nve-button interaction="emphasis">Group action</nve-button></nve-notification-group>
<nve-page-loader><nve-button interaction="emphasis">Loader action</nve-button></nve-page-loader>
<nve-toast><nve-button interaction="emphasis">Toast action</nve-button></nve-toast>
<nve-toggletip><nve-button interaction="emphasis">Toggletip action</nve-button></nve-toggletip>
<nve-tooltip><nve-button interaction="emphasis">Tooltip action</nve-button></nve-tooltip>`
],
invalid: []
});
});

it('should continue counting emphasis buttons outside popover elements', () => {
tester.run('page emphasis buttons around popovers', rule, {
valid: [],
invalid: [
{
code: `<nve-button interaction="emphasis">Page action one</nve-button>
<nve-dialog>
<nve-button interaction="emphasis">Dialog action</nve-button>
</nve-dialog>
<nve-button interaction="emphasis">Page action two</nve-button>
<nve-button interaction="emphasis">Page action three</nve-button>`,
errors: [error]
}
]
});
});

it('should count separate tagged templates independently', () => {
const javascriptTester = new RuleTester({
languageOptions: {
Expand Down
21 changes: 20 additions & 1 deletion projects/lint/src/eslint/rules/no-excessive-primary-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,29 @@
import type { Rule } from 'eslint';
import { createVisitors } from '@html-eslint/eslint-plugin/lib/rules/utils/visitors.js';
import { findAttr } from '@html-eslint/eslint-plugin/lib/rules/utils/node.js';
import { elements } from '../internals/metadata.js';
import type { HtmlTagNode } from '../rule-types.js';

declare const __ELEMENTS_PAGES_BASE_URL__: string;
const MAX_EMPHASIS_BUTTONS = 2;
const POPOVER_ELEMENTS: ReadonlySet<string> = new Set(
elements
.filter(element => element.manifest?.metadata?.behavior === 'popover')
.map(element => element.name.toLowerCase())
);

function hasPopoverAncestor(node: HtmlTagNode): boolean {
let current = node.parent;

while (current) {
if (current.name && POPOVER_ELEMENTS.has(current.name.toLowerCase())) {
return true;
}
current = current.parent;
}

return false;
}

const rule = {
meta: {
Expand All @@ -32,7 +51,7 @@ const rule = {
emphasisButtonCount = 0;
},
Tag(node: HtmlTagNode) {
if (node.name.toLowerCase() !== 'nve-button') {
if (node.name.toLowerCase() !== 'nve-button' || hasPopoverAncestor(node)) {
return;
}

Expand Down
27 changes: 27 additions & 0 deletions projects/site/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,33 @@ export default [
...browserTypescriptConfig,
...appConfig,
...jsonConfig,
{
// These examples intentionally demonstrate incomplete and hypothetical component APIs.
files: ['src/docs/api-design/**/*.md', 'src/docs/internal/guidelines/**/*.md'],
rules: {
'@nvidia-elements/lint/no-missing-control-label': 'off',
'@nvidia-elements/lint/no-missing-slotted-elements': 'off',
'@nvidia-elements/lint/no-unknown-tags': 'off'
}
},
{
// These examples intentionally demonstrate deprecated APIs.
files: ['src/docs/about/migration.md'],
rules: {
'@nvidia-elements/lint/no-deprecated-tags': 'off',
'@nvidia-elements/lint/no-deprecated-attributes': 'off',
'@nvidia-elements/lint/no-deprecated-global-attributes': 'off',
'@nvidia-elements/lint/no-deprecated-popover-attributes': 'off',
'@nvidia-elements/lint/no-deprecated-icon-names': 'off',
'@nvidia-elements/lint/no-unexpected-attribute-value': 'off',
'@nvidia-elements/lint/no-unstyled-typography': 'off',
'@nvidia-elements/lint/no-missing-control-label': 'off',
'@nvidia-elements/lint/no-missing-slotted-elements': 'off',
'@nvidia-elements/lint/no-unexpected-global-attribute-value': 'off',
'@nvidia-elements/lint/no-restricted-container-full': 'off',
'@nvidia-elements/lint/no-unknown-tags': 'off'
}
},
{
files: ['src/_11ty/**/*.js'],
rules: {
Expand Down
1 change: 1 addition & 0 deletions projects/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
"command": "eslint -c ./eslint.config.js --color --cache --cache-location .eslintcache/",
"files": [
"src/**/*.js",
"src/**/*.md",
"src/**/*.ts",
"eslint.config.js"
],
Expand Down
20 changes: 14 additions & 6 deletions projects/site/src/docs/api-design/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Elements should default to using composition when possible. This approach is to

```html
<nve-button>
button <nve-icon name="info"></nve-icon>
button <nve-icon name="information-circle"></nve-icon>
</nve-button>
```

Expand All @@ -36,7 +36,7 @@ Going further this runs into layout conflicts. If the icon needs to change posit

```html
<nve-button>
<nve-icon name="info"></nve-icon> button
<nve-icon name="information-circle"></nve-icon> button
</nve-button>
```

Expand Down Expand Up @@ -64,6 +64,8 @@ Elements should provide reasonable defaults for better developer experience for

The alert can internally provide the default icon style for the status in the system. But as above with the button, the alert element runs the risk of absorbing parts of the icon API. To mitigate this, use a documented named slot as the customization hook.

<!-- eslint-disable @nvidia-elements/lint/no-unexpected-attribute-value -->

```html
<!-- nve-alert template -->
<div>
Expand All @@ -80,12 +82,16 @@ The alert can internally provide the default icon style for the status in the sy
</nve-alert>
```

<!-- eslint-enable @nvidia-elements/lint/no-unexpected-attribute-value -->

Slots can provide default content if the consumer supplies no content. Here the template sets an internal icon with a status icon that matches the status of the alert. If the consumer wants to customize the icon, they can project their own icon into the `icon` slot and override the default. This makes `icon` an explicit public slot API, while avoiding a series of icon-specific inherited attributes or properties on the alert.

## Semantic Obfuscation - anti-pattern

When building composition based APIs the developer should push the semantics of the HTML up into the light DOM or the control of the consumer. In this example the card element embeds the h1 heading. This creates an incorrect DOM structure as only one given h1 can exist within the page. This also applies as the page structure should work down from h1-h6.

<!-- eslint-disable @nvidia-elements/lint/no-unexpected-slot-value -->

{% dodont %}

```html
Expand All @@ -100,15 +106,15 @@ When building composition based APIs the developer should push the semantics of

<!-- consumer API -->
<nve-card status="warning">
<h2 slot="header">Card Header</h2>
<p>card content</p>
<h2 slot="header" nve-text="heading">Card Header</h2>
<p nve-text="body">card content</p>
</nve-card>
```

```html
<!-- nve-card template -->
<div>
<h1><slot name="header"></slot></h1>
<h1 nve-text="heading"><slot name="header"></slot></h1>
<div>
<slot></slot>
</div>
Expand All @@ -118,12 +124,14 @@ When building composition based APIs the developer should push the semantics of
<!-- consumer API -->
<nve-card status="warning">
<div slot="header">Card Header</div>
<p>card content</p>
<p nve-text="body">card content</p>
</nve-card>
```

{% enddodont %}

<!-- eslint-enable @nvidia-elements/lint/no-unexpected-slot-value -->

While composition based APIs may be more verbose at times, they lower the API surface area to learn in the system and help ensure there is a singular way to use the element. Once a consumer learns an element API, that API usage remains predictable and reliable throughout the system.

Consumer apps/plugins can add opinionated abstractions. This can provide a more opinionated terse API in which consumers can always “escape” or access the elements of the base library as needed. It's easier to add abstraction layers, it's much more difficult to pull apart the wrong base abstraction.
16 changes: 8 additions & 8 deletions projects/site/src/docs/api-design/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ This document is not intended to define the best practices and API design of hig
<nve-alert status="danger">Don't: a practice to avoid</nve-alert>
<nve-alert status="accent">Tip: helpful details on rationale for a given guideline</nve-alert>
<nve-alert status="warning">details on the risks of not following a guideline</nve-alert>
<nve-alert><nve-icon slot="icon">🏁</nve-icon> Performance: detail about how a guideline impacts performance</nve-alert>
<nve-alert><nve-icon slot="icon">🎓</nve-icon> Learn: resource to learn more about a guideline topic</nve-alert>
<nve-alert><nve-icon slot="icon">🚧</nve-icon> WIP: details on any work in progress guidance</nve-alert>
<nve-alert><nve-icon slot="icon" name="flag"></nve-icon> Performance: detail about how a guideline impacts performance</nve-alert>
<nve-alert><nve-icon slot="icon" name="academic-cap"></nve-icon> Learn: resource to learn more about a guideline topic</nve-alert>
<nve-alert><nve-icon slot="icon" name="traffic-cone"></nve-icon> WIP: details on any work in progress guidance</nve-alert>
</div>

## Terminology
Expand Down Expand Up @@ -70,7 +70,7 @@ Consistent element APIs provide consistent developer experience. The recommendat
```html
<!-- HTML/JavaScript -->
<nve-alert status="success">
<p>hello there!</p>
<p nve-text="body">hello there!</p>
</nve-alert>

<script type="module">
Expand All @@ -82,22 +82,22 @@ Consistent element APIs provide consistent developer experience. The recommendat

<!-- Angular -->
<nve-alert status="success" [closable]="boolProp" (close)="handle($event)">
<p>hello there!</p>
<p nve-text="body">hello there!</p>
</nve-alert>

<!-- Lit -->
<nve-alert status="success" ?closable=${boolProp} @close=${e => this.handle(e)}>
<p>hello there!</p>
<p nve-text="body">hello there!</p>
</nve-alert>

<!-- Vue -->
<nve-alert status="success" :closable="boolProp" @close="handle">
<p>hello there!</p>
<p nve-text="body">hello there!</p>
</nve-alert>

<!-- React/Preact -->
<nve-alert status="success" closable={this.state.boolProp} onClose={this.handle}>
<p>hello there!</p>
<p nve-text="body">hello there!</p>
</nve-alert>
```

Expand Down
8 changes: 6 additions & 2 deletions projects/site/src/docs/api-design/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ This warning appears when invalid elements are slotted into a component. Example

```html
<nve-tree>
<nve-tree-node></nve-tree-node>
<nve-tree-node>Node</nve-tree-node>
</nve-tree>

<nve-grid-row>
Expand All @@ -73,7 +73,7 @@ This warning appears when invalid elements are slotted into a component. Example
```html
<nve-tree>
<div>
<nve-tree-node></nve-tree-node>
<nve-tree-node>Node</nve-tree-node>
</div>
</nve-tree>

Expand Down Expand Up @@ -102,11 +102,15 @@ This warning appears when a component tries to reference an element by ID that d

<nve-alert status="danger">Invalid</nve-alert>

<!-- eslint-disable @nvidia-elements/lint/no-missing-popover-trigger -->

```html
<nve-button popovertarget="my-popover">show tooltip</nve-button>
<nve-tooltip id="incorrect-id">tooltip</nve-tooltip>
```

<!-- eslint-enable @nvidia-elements/lint/no-missing-popover-trigger -->

To resolve this warning:

- Ensure the referenced element exists in the DOM
Expand Down
8 changes: 4 additions & 4 deletions projects/site/src/docs/api-design/packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Build outputs should target the latest ES2020+. Build outputs should not compile

<nve-alert status="warning">Warning: avoid alternate build targets as it increases complexity and is unnecessary with modern build tools and browsers.</nve-alert>

<nve-alert><nve-icon slot="icon">🎓</nve-icon> Learn: <a href="https://justinfagnani.com/2019/11/01/how-to-publish-web-components-to-npm/">publishing Web Components</a></nve-alert>
<nve-alert><nve-icon slot="icon" name="academic-cap"></nve-icon> Learn: <a href="https://justinfagnani.com/2019/11/01/how-to-publish-web-components-to-npm/">publishing Web Components</a></nve-alert>

## Entrypoints

Expand Down Expand Up @@ -95,8 +95,8 @@ The `package.json` should use a `sideEffects` array that lists registration and

This enables tools like Webpack and Rollup to preserve explicit registration entrypoints while still tree-shaking side-effect-free component modules.

<nve-alert><nve-icon slot="icon">🎓</nve-icon> Learn about <a href="/docs/integrations/lit-library/">Lit Library integration</a></nve-alert>
<nve-alert><nve-icon slot="icon" name="academic-cap"></nve-icon> Learn about <a href="/docs/integrations/lit-library/">Lit Library integration</a></nve-alert>

<nve-alert><nve-icon slot="icon">🎓</nve-icon> Learn: <a href="https://github.com/webcomponents/polyfills/tree/master/packages/scoped-custom-element-registry">scoped element registry</a></nve-alert>
<nve-alert><nve-icon slot="icon" name="academic-cap"></nve-icon> Learn: <a href="https://github.com/webcomponents/polyfills/tree/master/packages/scoped-custom-element-registry">scoped element registry</a></nve-alert>

<nve-alert><nve-icon slot="icon">🎓</nve-icon> Learn: <a href="https://www.youtube.com/watch?v=QmDToR6mLhk">case study</a></nve-alert>
<nve-alert><nve-icon slot="icon" name="academic-cap"></nve-icon> Learn: <a href="https://www.youtube.com/watch?v=QmDToR6mLhk">case study</a></nve-alert>
Loading