diff --git a/NOTICE.md b/NOTICE.md
index 77d41919ed..67dc235843 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -70,7 +70,7 @@ This product includes the following bundled third-party software:
- ignore v7.0.6 [MIT] (used by: @nvidia-elements/pi)
Copyright: kael
-- lit v3.3.3 [BSD-3-Clause] (used by: @nvidia-elements/code, @nvidia-elements/core, @nvidia-elements/markdown, @nvidia-elements/media, @nvidia-elements/monaco, @nvidia-elements/plot, @nvidia-elements/scene)
+- lit v3.3.3 [BSD-3-Clause] (used by: @nvidia-elements/code, @nvidia-elements/core, @nvidia-elements/markdown, @nvidia-elements/media, @nvidia-elements/monaco, @nvidia-elements/plot, @nvidia-elements/scene, @nvidia-elements/viewport)
Copyright: Google LLC
- lit-html v3.3.3 [BSD-3-Clause] (used by: @nvidia-elements/code, @nvidia-elements/core, @nvidia-elements/monaco)
diff --git a/commitlint.config.js b/commitlint.config.js
index 3e5b47dc95..371c4a5b43 100644
--- a/commitlint.config.js
+++ b/commitlint.config.js
@@ -37,7 +37,8 @@ export default {
'markdown',
'media',
'monaco',
- 'scene'
+ 'scene',
+ 'viewport'
]
]
}
diff --git a/knip.config.js b/knip.config.js
index 0f19fee932..ec6d506878 100644
--- a/knip.config.js
+++ b/knip.config.js
@@ -236,10 +236,23 @@ export default {
],
project: [...PROJECT_FILES, ...SOURCE_FILES]
},
+ 'projects/viewport': {
+ entry: [
+ ...PACKAGE_FILES,
+ ...SOURCE_INDEX,
+ ...DEFINE_ENTRIES,
+ ...EXAMPLE_ENTRIES,
+ ...TEST_ENTRIES,
+ ...TEST_VARIANT_ENTRIES,
+ ...VITE_CONFIGS,
+ ...VITEST_CONFIGS
+ ],
+ project: [...PROJECT_FILES, ...SOURCE_FILES]
+ },
'projects/site': {
entry: [...PACKAGE_FILES, 'src/**/*.{js,ts}', ...VITEST_CONFIGS],
project: [...PACKAGE_FILES, 'src/**/*.{js,ts}', ...VITEST_CONFIGS],
- ignoreDependencies: ['@nvidia-elements/plot', '@nvidia-elements/scene'] // temporary
+ ignoreDependencies: ['@nvidia-elements/plot', '@nvidia-elements/scene', '@nvidia-elements/viewport']
},
'projects/styles': {
entry: [
diff --git a/package.json b/package.json
index 342b03ae33..98e5e6fcbe 100644
--- a/package.json
+++ b/package.json
@@ -90,6 +90,7 @@
"./projects/pi:ci",
"./projects/plot:ci",
"./projects/scene:ci",
+ "./projects/viewport:ci",
"./projects/site:ci",
"./projects/starters:ci",
"./projects/starters/angular:ci",
@@ -145,6 +146,7 @@
"./projects/monaco:test:lighthouse",
"./projects/plot:test:lighthouse",
"./projects/scene:test:lighthouse",
+ "./projects/viewport:test:lighthouse",
"./projects/site:test:lighthouse"
],
"output": [
@@ -172,7 +174,8 @@
"release:media",
"release:monaco",
"release:pi",
- "release:styles"
+ "release:styles",
+ "release:viewport"
]
},
"release:themes": {
@@ -245,6 +248,13 @@
"release:themes"
]
},
+ "release:viewport": {
+ "command": "cd projects/viewport && HUSKY=0 pnpm exec semantic-release",
+ "dependencies": [
+ "release:core",
+ "release:themes"
+ ]
+ },
"format": {
"command": "prettier --config prettier.config.js --check --cache .",
"files": [
@@ -305,6 +315,7 @@
"./projects/media:lint:fix",
"./projects/monaco:lint:fix",
"./projects/scene:lint:fix",
+ "./projects/viewport:lint:fix",
"./projects/site:lint:fix"
]
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5d2f49ecd2..a5c09660fe 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1734,6 +1734,9 @@ importers:
'@nvidia-elements/themes':
specifier: workspace:*
version: link:../themes
+ '@nvidia-elements/viewport':
+ specifier: workspace:*
+ version: link:../viewport
'@vitest/browser':
specifier: 'catalog:'
version: 5.0.0(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)
@@ -2715,6 +2718,70 @@ importers:
specifier: 'catalog:'
version: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.4.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-istanbul@5.0.0)(@vitest/ui@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))
+ projects/viewport:
+ dependencies:
+ '@nvidia-elements/core':
+ specifier: workspace:^
+ version: link:../core
+ '@nvidia-elements/themes':
+ specifier: workspace:^
+ version: link:../themes
+ lit:
+ specifier: catalog:publish
+ version: 3.3.3
+ devDependencies:
+ '@eslint/js':
+ specifier: 'catalog:'
+ version: 10.0.1(eslint@10.11.0(jiti@2.7.0)(supports-color@10.2.2))
+ '@internals/eslint':
+ specifier: workspace:*
+ version: link:../internals/eslint
+ '@internals/testing':
+ specifier: workspace:*
+ version: link:../internals/testing
+ '@internals/vite':
+ specifier: workspace:*
+ version: link:../internals/vite
+ '@nvidia-elements/lint':
+ specifier: workspace:*
+ version: link:../lint
+ '@nvidia-elements/styles':
+ specifier: workspace:*
+ version: link:../styles
+ '@vitest/browser':
+ specifier: 'catalog:'
+ version: 5.0.0(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))(vitest@5.0.0)
+ '@vitest/coverage-istanbul':
+ specifier: 'catalog:'
+ version: 5.0.0(vitest@5.0.0)
+ axe-core:
+ specifier: 'catalog:'
+ version: 4.11.0
+ eslint:
+ specifier: 10.11.0
+ version: 10.11.0(jiti@2.7.0)(supports-color@10.2.2)
+ publint:
+ specifier: 'catalog:'
+ version: 0.3.24
+ stylelint:
+ specifier: 'catalog:'
+ version: 17.14.1(supports-color@10.2.2)(typescript@6.0.3)
+ stylelint-config-standard:
+ specifier: 'catalog:'
+ version: 40.0.0(stylelint@17.14.1(supports-color@10.2.2)(typescript@6.0.3))
+ typescript:
+ specifier: 'catalog:'
+ version: 6.0.3
+ typescript-7:
+ specifier: 'catalog:'
+ version: typescript@7.0.2
+ vite:
+ specifier: 'catalog:'
+ version: 8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0)
+ vitest:
+ specifier: 'catalog:'
+ version: 5.0.0(@opentelemetry/api@1.9.1)(@types/node@26.5.0)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-istanbul@5.0.0)(@vitest/ui@5.0.0)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.5.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0))
+
packages:
3d-force-graph@1.79.0:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 21d6956a34..5eebe93e8f 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -35,6 +35,7 @@ packages:
- projects/forms
- projects/plot
- projects/scene
+ - projects/viewport
- projects/styles
- projects/themes
- projects/internals/design
diff --git a/projects/core/.visual/viewport-crosses.dark.png b/projects/core/.visual/viewport-crosses.dark.png
deleted file mode 100644
index 1684609ed0..0000000000
--- a/projects/core/.visual/viewport-crosses.dark.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:48bcd15fd04da7b051abe45e13091fbde555288671024f3b24db04a729f3ef9e
-size 3941
diff --git a/projects/core/.visual/viewport-crosses.png b/projects/core/.visual/viewport-crosses.png
deleted file mode 100644
index fdb15b0d7e..0000000000
--- a/projects/core/.visual/viewport-crosses.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:552759fa2e17bb6c3640fcf7469be4ad1bbaae8eb29ff001984a0c26c2758804
-size 3905
diff --git a/projects/core/.visual/viewport-dots.dark.png b/projects/core/.visual/viewport-dots.dark.png
deleted file mode 100644
index 94ff41d22c..0000000000
--- a/projects/core/.visual/viewport-dots.dark.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:3281787e6ab1995abe5f7dcc8aec6b73db967515f666263153fb914db40043c9
-size 3997
diff --git a/projects/core/.visual/viewport-dots.png b/projects/core/.visual/viewport-dots.png
deleted file mode 100644
index 874487955a..0000000000
--- a/projects/core/.visual/viewport-dots.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:22fc3b1ad55c7d499011c97dfdde247ae37587b08896f8e39f9ce37c37a1ba4c
-size 3978
diff --git a/projects/core/.visual/viewport-minimap-custom.dark.png b/projects/core/.visual/viewport-minimap-custom.dark.png
deleted file mode 100644
index 1bb4504d12..0000000000
--- a/projects/core/.visual/viewport-minimap-custom.dark.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:44762dc3f8b5385a9da084b8f5aa5aee4a3e3ef885dfbfdadac82a8200c905d8
-size 5162
diff --git a/projects/core/.visual/viewport-minimap-custom.png b/projects/core/.visual/viewport-minimap-custom.png
deleted file mode 100644
index a4b3490538..0000000000
--- a/projects/core/.visual/viewport-minimap-custom.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:3210794387f365ca867f9cbd221e45513b48ff6d117916e97dadafe9a083268d
-size 5258
diff --git a/projects/core/.visual/viewport-minimap.dark.png b/projects/core/.visual/viewport-minimap.dark.png
deleted file mode 100644
index f3e0313809..0000000000
--- a/projects/core/.visual/viewport-minimap.dark.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:a2f4a9494a842136f7eb2ffc23415ed0961f75bdf3ff65a30ab4c5e194f2fb14
-size 4573
diff --git a/projects/core/.visual/viewport-minimap.png b/projects/core/.visual/viewport-minimap.png
deleted file mode 100644
index de265d1240..0000000000
--- a/projects/core/.visual/viewport-minimap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:d0c04c7f2dba83f903cb29ed067f0a2d94e58ba65e2105881bb658291fc2ebe8
-size 4629
diff --git a/projects/core/.visual/viewport.dark.png b/projects/core/.visual/viewport.dark.png
deleted file mode 100644
index 053545141c..0000000000
--- a/projects/core/.visual/viewport.dark.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:e5d596b575ceaf1d57da0dc1a6eec0307775cbdfb9c7b2591de0f5c143004083
-size 3722
diff --git a/projects/core/.visual/viewport.png b/projects/core/.visual/viewport.png
deleted file mode 100644
index 57f4848f60..0000000000
--- a/projects/core/.visual/viewport.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:75916012a4a87e021c1e43e3529862ccae321c58772205cf0550314714fa05b1
-size 3681
diff --git a/projects/core/package.json b/projects/core/package.json
index b07c21d131..8954ea3fe1 100644
--- a/projects/core/package.json
+++ b/projects/core/package.json
@@ -951,18 +951,6 @@
"types": "./dist/tree/define.d.ts",
"default": "./dist/tree/define.js"
},
- "./viewport": {
- "types": "./dist/viewport/index.d.ts",
- "default": "./dist/viewport/index.js"
- },
- "./viewport/index.js": {
- "types": "./dist/viewport/index.d.ts",
- "default": "./dist/viewport/index.js"
- },
- "./viewport/define.js": {
- "types": "./dist/viewport/define.d.ts",
- "default": "./dist/viewport/define.js"
- },
"./week": {
"types": "./dist/week/index.d.ts",
"default": "./dist/week/index.js"
diff --git a/projects/core/src/bundle.ts b/projects/core/src/bundle.ts
index 7beaf3af91..fb8c672ed1 100644
--- a/projects/core/src/bundle.ts
+++ b/projects/core/src/bundle.ts
@@ -74,7 +74,6 @@ import '@nvidia-elements/core/toggletip/define.js';
import '@nvidia-elements/core/toolbar/define.js';
import '@nvidia-elements/core/tooltip/define.js';
import '@nvidia-elements/core/tree/define.js';
-import '@nvidia-elements/core/viewport/define.js';
import '@nvidia-elements/core/week/define.js';
export * from '@nvidia-elements/core/accordion';
@@ -147,7 +146,6 @@ export * from '@nvidia-elements/core/toggletip';
export * from '@nvidia-elements/core/toolbar';
export * from '@nvidia-elements/core/tooltip';
export * from '@nvidia-elements/core/tree';
-export * from '@nvidia-elements/core/viewport';
export * from '@nvidia-elements/core/week';
globalThis.NVE_ELEMENTS.state.bundle = true;
diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts
index e414d24e27..3ed158ce18 100644
--- a/projects/core/src/index.test.lighthouse.ts
+++ b/projects/core/src/index.test.lighthouse.ts
@@ -99,7 +99,6 @@ describe('lighthouse report', () => {
import '@nvidia-elements/core/toolbar/define.js';
import '@nvidia-elements/core/tooltip/define.js';
import '@nvidia-elements/core/tree/define.js';
- import '@nvidia-elements/core/viewport/define.js';
import '@nvidia-elements/core/week/define.js';
`
diff --git a/projects/core/src/viewport/viewport.test.ssr.ts b/projects/core/src/viewport/viewport.test.ssr.ts
deleted file mode 100644
index 203d019ed9..0000000000
--- a/projects/core/src/viewport/viewport.test.ssr.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// 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 { Viewport } from '@nvidia-elements/core/viewport';
-import '@nvidia-elements/core/viewport/define.js';
-
-describe(Viewport.metadata.tag, () => {
- it('should render content layers and assign viewport companions on the server', async () => {
- const result = await ssrRunner.render(html`
-
-
-
- content
-
- `);
-
- expect(result.includes('shadowroot="open"')).toBe(true);
- expect(result.includes('slot name="background"')).toBe(true);
- expect(result.includes('slot name="overlay"')).toBe(true);
- expect(result.includes('class="plane"')).toBe(true);
- const gridlinesTag = result.match(/]*>/)?.[0];
- expect(gridlinesTag).toBeDefined();
- expect(gridlinesTag).toContain('slot="background"');
- expect(gridlinesTag).toContain('aria-hidden="true"');
- const minimapTag = result.match(/]*>/)?.[0];
- expect(minimapTag).toBeDefined();
- expect(minimapTag).toContain('slot="overlay"');
- expect(minimapTag).toContain('aria-hidden="true"');
- });
-});
diff --git a/projects/internals/metadata/package.json b/projects/internals/metadata/package.json
index 99980efbe7..d581824453 100644
--- a/projects/internals/metadata/package.json
+++ b/projects/internals/metadata/package.json
@@ -118,6 +118,8 @@
"../../media/dist/custom-elements.json",
"../../monaco/package.json",
"../../monaco/dist/custom-elements.json",
+ "../../viewport/package.json",
+ "../../viewport/dist/custom-elements.json",
"../../patterns/package.json",
"../../patterns/dist/custom-elements.json",
"../../plot/dist/custom-elements.json",
@@ -168,6 +170,10 @@
{
"script": "../../scene:build",
"cascade": false
+ },
+ {
+ "script": "../../viewport:build",
+ "cascade": false
}
]
},
@@ -250,6 +256,11 @@
"../../monaco/coverage/unit/summary.json",
"../../monaco/coverage/visual/summary.json",
"../../monaco/coverage/ssr/summary.json",
+ "../../viewport/coverage/unit/coverage-summary.json",
+ "../../viewport/coverage/unit/summary.json",
+ "../../viewport/coverage/axe/summary.json",
+ "../../viewport/coverage/visual/summary.json",
+ "../../viewport/coverage/ssr/summary.json",
"../tools/coverage/unit/coverage-summary.json",
"../tools/coverage/unit/summary.json"
],
@@ -313,6 +324,22 @@
"script": "../../monaco:test:coverage",
"cascade": false
},
+ {
+ "script": "../../viewport:test:coverage",
+ "cascade": false
+ },
+ {
+ "script": "../../viewport:test:axe",
+ "cascade": false
+ },
+ {
+ "script": "../../viewport:test:ssr",
+ "cascade": false
+ },
+ {
+ "script": "../../viewport:test:visual",
+ "cascade": false
+ },
{
"script": "../../cli:test:coverage",
"cascade": false
@@ -343,7 +370,8 @@
"../../forms/dist/**/*.examples.json",
"../../markdown/dist/**/*.examples.json",
"../../media/dist/**/*.examples.json",
- "../../scene/dist/**/*.examples.json"
+ "../../scene/dist/**/*.examples.json",
+ "../../viewport/dist/**/*.examples.json"
],
"output": [
"static/examples.json"
@@ -385,6 +413,10 @@
"script": "../../scene:build",
"cascade": false
},
+ {
+ "script": "../../viewport:build",
+ "cascade": false
+ },
{
"script": "../patterns:build",
"cascade": false
@@ -408,7 +440,8 @@
"clean": false,
"files": [
"src/tasks/lighthouse.ts",
- "src/tasks/lighthouse.utils.ts"
+ "src/tasks/lighthouse.utils.ts",
+ "../../viewport/.lighthouse/dist/report.json"
],
"output": [
"static/lighthouse.json"
diff --git a/projects/internals/metadata/src/tasks/api.utils.ts b/projects/internals/metadata/src/tasks/api.utils.ts
index 0242f85f59..b0712ceaa3 100644
--- a/projects/internals/metadata/src/tasks/api.utils.ts
+++ b/projects/internals/metadata/src/tasks/api.utils.ts
@@ -136,7 +136,8 @@ export async function getApi(): Promise<{
'../../../../media',
'../../../../monaco',
'../../../../plot',
- '../../../../scene'
+ '../../../../scene',
+ '../../../../viewport'
];
const types = getElementsStandardAPIProperties();
diff --git a/projects/internals/metadata/src/tasks/lighthouse.utils.test.ts b/projects/internals/metadata/src/tasks/lighthouse.utils.test.ts
index e5252de1bc..ed26e12838 100644
--- a/projects/internals/metadata/src/tasks/lighthouse.utils.test.ts
+++ b/projects/internals/metadata/src/tasks/lighthouse.utils.test.ts
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
+import { existsSync, readFileSync } from 'node:fs';
import { beforeEach, describe, expect, it } from 'vitest';
import { type LighthouseScores, generateLighthouseReport, getLighthouseReport } from './lighthouse.utils.js';
@@ -44,4 +45,12 @@ describe('Lighthouse Metadata', () => {
expect(report.created).toBeDefined();
expect(report['@nvidia-elements/core']).not.toBe(undefined);
});
+
+ it('should preserve viewport component scores from the package report', async () => {
+ const reportPath = new URL('../../../../viewport/.lighthouse/dist/report.json', import.meta.url);
+ const expected = existsSync(reportPath) ? JSON.parse(readFileSync(reportPath, 'utf8')) : null;
+ const report = await generateLighthouseReport();
+
+ expect(report).toHaveProperty('@nvidia-elements/viewport', expected);
+ });
});
diff --git a/projects/internals/metadata/src/tasks/lighthouse.utils.ts b/projects/internals/metadata/src/tasks/lighthouse.utils.ts
index 112901e9d6..268be55afc 100644
--- a/projects/internals/metadata/src/tasks/lighthouse.utils.ts
+++ b/projects/internals/metadata/src/tasks/lighthouse.utils.ts
@@ -18,6 +18,7 @@ export interface LighthouseScores {
'@nvidia-elements/markdown': { [key: string]: LighthouseElementReport };
'@nvidia-elements/media': { [key: string]: LighthouseElementReport };
'@nvidia-elements/monaco': { [key: string]: LighthouseElementReport };
+ '@nvidia-elements/viewport': { [key: string]: LighthouseElementReport };
}
interface LighthouseElementReport {
@@ -59,7 +60,8 @@ export async function generateLighthouseReport(): Promise {
'@nvidia-elements/forms': await loadLighthouseReport('../../../../forms'),
'@nvidia-elements/markdown': await loadLighthouseReport('../../../../markdown'),
'@nvidia-elements/media': await loadLighthouseReport('../../../../media'),
- '@nvidia-elements/monaco': await loadLighthouseReport('../../../../monaco')
+ '@nvidia-elements/monaco': await loadLighthouseReport('../../../../monaco'),
+ '@nvidia-elements/viewport': await loadLighthouseReport('../../../../viewport')
};
}
diff --git a/projects/internals/metadata/src/tasks/tests.utils.test.ts b/projects/internals/metadata/src/tasks/tests.utils.test.ts
index da960e4680..7291391647 100644
--- a/projects/internals/metadata/src/tasks/tests.utils.test.ts
+++ b/projects/internals/metadata/src/tasks/tests.utils.test.ts
@@ -325,6 +325,7 @@ describe('generateTestSummary', () => {
'@nvidia-elements/markdown',
'@nvidia-elements/media',
'@nvidia-elements/monaco',
+ '@nvidia-elements/viewport',
'@nvidia-elements/pi',
'@internals/metadata',
'@internals/patterns',
diff --git a/projects/internals/metadata/src/tasks/tests.utils.ts b/projects/internals/metadata/src/tasks/tests.utils.ts
index 1cb4ac6ce8..13401e7cce 100644
--- a/projects/internals/metadata/src/tasks/tests.utils.ts
+++ b/projects/internals/metadata/src/tasks/tests.utils.ts
@@ -103,6 +103,7 @@ export async function generateTestSummary(): Promise {
'@nvidia-elements/markdown': await getTestReport('@nvidia-elements/markdown', '../../../../markdown'),
'@nvidia-elements/media': await getTestReport('@nvidia-elements/media', '../../../../media'),
'@nvidia-elements/monaco': await getTestReport('@nvidia-elements/monaco', '../../../../monaco'),
+ '@nvidia-elements/viewport': await getTestReport('@nvidia-elements/viewport', '../../../../viewport'),
'@nvidia-elements/pi': await getTestReport('@nvidia-elements/pi', '../../../../pi'),
'@internals/metadata': await getTestReport('@internals/metadata', '../../../../internals/metadata'),
'@internals/patterns': await getTestReport('@internals/patterns', '../../../../internals/patterns'),
diff --git a/projects/internals/metadata/static/adoption.json b/projects/internals/metadata/static/adoption.json
index ede0c1f81c..96983ec1b9 100644
--- a/projects/internals/metadata/static/adoption.json
+++ b/projects/internals/metadata/static/adoption.json
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:630324999d28fb2d962f39ee7867877ca3b17175a5a017b0029ff86dab7e860e
-size 261417
+oid sha256:9d01ba96d7bb402c40adeb9e21ddd3ce921f9f1254dcffd10e27efa2dfa3c619
+size 409449
diff --git a/projects/internals/metadata/static/lighthouse.json b/projects/internals/metadata/static/lighthouse.json
index 3fc5b80a93..e13ca1e9d1 100644
--- a/projects/internals/metadata/static/lighthouse.json
+++ b/projects/internals/metadata/static/lighthouse.json
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:626431290e683873d47f224cb668634c11956b60aed788299f40d4a8aaf82869
-size 67679
+oid sha256:643742940aca2a120418515e31c05ab99a316089baba12f972c605f00063280a
+size 71920
diff --git a/projects/internals/metadata/static/releases.json b/projects/internals/metadata/static/releases.json
index fea0234cc0..d88b9164a8 100644
--- a/projects/internals/metadata/static/releases.json
+++ b/projects/internals/metadata/static/releases.json
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:62136236308eb17156d2ce572a205e19cb510a48e06e4743f287b279de19b020
-size 23960
+oid sha256:434936d6c3004f25b64e5b7e795b76a377f9b5751e3764e01055ef694675980b
+size 28279
diff --git a/projects/internals/metadata/static/tests.json b/projects/internals/metadata/static/tests.json
index 7eede6f5c3..cb3add4cd3 100644
--- a/projects/internals/metadata/static/tests.json
+++ b/projects/internals/metadata/static/tests.json
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ce71d876f17db545219fbb9699b717b944774b1839fcc2aae481684d11f84cd4
-size 2947869
+oid sha256:5ba2a4628bf25704b6531c072cded88d3ab4a027b30e551c0799aedd62352856
+size 3377501
diff --git a/projects/site/eleventy.config.js b/projects/site/eleventy.config.js
index d6bd63eb57..3e34d45ea8 100644
--- a/projects/site/eleventy.config.js
+++ b/projects/site/eleventy.config.js
@@ -240,7 +240,8 @@ export default function (eleventyConfig) {
'src/docs/code/*.md',
'src/docs/monaco/*.md',
'src/docs/media/*.md',
- 'src/docs/markdown/index.md'
+ 'src/docs/markdown/index.md',
+ 'src/docs/viewport/*.md'
])
.filter(page => page.data.tag);
});
diff --git a/projects/site/package.json b/projects/site/package.json
index ee15bd38da..0eed1b5eca 100644
--- a/projects/site/package.json
+++ b/projects/site/package.json
@@ -106,6 +106,8 @@
"../plot/dist/**/*.examples.json",
"../scene/dist/**/*.js",
"../scene/dist/**/*.examples.json",
+ "../viewport/dist/**/*.js",
+ "../viewport/dist/**/*.examples.json",
"../internals/design/dist/DESIGN.md",
"../internals/metadata/static/**",
"../internals/patterns/dist/**/*.js",
@@ -173,6 +175,10 @@
{
"script": "../internals/tools:build",
"cascade": false
+ },
+ {
+ "script": "../viewport:build",
+ "cascade": false
}
],
"output": []
@@ -346,6 +352,7 @@
"@nvidia-elements/scene": "workspace:*",
"@nvidia-elements/styles": "workspace:*",
"@nvidia-elements/themes": "workspace:*",
+ "@nvidia-elements/viewport": "workspace:*",
"@vitest/browser": "catalog:",
"@vitest/coverage-istanbul": "catalog:",
"compare-versions": "6.1.1",
diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js
index 08617b8716..381c6043bf 100644
--- a/projects/site/src/_11ty/layouts/common.js
+++ b/projects/site/src/_11ty/layouts/common.js
@@ -240,7 +240,7 @@ export const renderDocsNav = data => /* html */ `
DESIGN.md
-
+
Elements
Accordion
Alert
@@ -370,7 +370,12 @@ export const renderDocsNav = data => /* html */ `
Toolbar
Tooltip
Tree
- Viewport
+
+ Viewport
+ Viewport
+ Gridlines
+ Minimap
+
Week
diff --git a/projects/site/src/_11ty/layouts/metadata.test.ts b/projects/site/src/_11ty/layouts/metadata.test.ts
index f822354d45..f091b2f984 100644
--- a/projects/site/src/_11ty/layouts/metadata.test.ts
+++ b/projects/site/src/_11ty/layouts/metadata.test.ts
@@ -500,6 +500,17 @@ describe('renderBaseHead', () => {
});
describe('renderDocsNav', () => {
+ it.each(
+ ['/docs/viewport/', '/docs/viewport/gridlines/', '/docs/viewport/minimap/'].flatMap(component =>
+ ['', 'api/', 'examples/'].map(tab => `${component}${tab}`)
+ )
+ )('should keep the elements and viewport groups expanded on %s', url => {
+ const html = renderDocsNav({ page: { url } });
+
+ expect(html).toMatch(/\s*Elements<\/a>/);
+ expect(html).toMatch(/\s*Viewport<\/a>/);
+ });
+
it('should expose DESIGN.md in getting started navigation', () => {
const html = renderDocsNav({ page: { url: '/docs/design-md/' } });
diff --git a/projects/site/src/docs/elements/_tabs/api.11ty.js b/projects/site/src/docs/elements/_tabs/api.11ty.js
index 02c09c1ecd..7377015da0 100644
--- a/projects/site/src/docs/elements/_tabs/api.11ty.js
+++ b/projects/site/src/docs/elements/_tabs/api.11ty.js
@@ -17,6 +17,7 @@ export const data = {
},
permalink: data => {
const filePath = data.component.filePathStem;
+ if (filePath.includes('/viewport/')) return `${data.component.url}api/`;
let dir = 'elements';
if (filePath.includes('/code/')) dir = 'code';
else if (filePath.includes('/monaco/')) dir = 'monaco';
diff --git a/projects/site/src/docs/elements/_tabs/examples.11ty.js b/projects/site/src/docs/elements/_tabs/examples.11ty.js
index 222aab548a..e706868b85 100644
--- a/projects/site/src/docs/elements/_tabs/examples.11ty.js
+++ b/projects/site/src/docs/elements/_tabs/examples.11ty.js
@@ -7,7 +7,7 @@ import { exampleDocShortcode } from '../../../_11ty/shortcodes/example-doc.js';
const { examples, elements } = siteData;
/**
- * @typedef {{component: {filePathStem: string, fileSlug: string, data: {tag?: string, title: string, hideExamplesTab?: boolean, page: {fileSlug: string}}}, page: {fileSlug: string}, tag?: string, title?: string, isExamplesTab?: boolean, noindex?: boolean}} ExamplesPageData
+ * @typedef {{component: {url: string, filePathStem: string, fileSlug: string, data: {tag?: string, title: string, hideExamplesTab?: boolean, page: {fileSlug: string}}}, page: {fileSlug: string}, tag?: string, title?: string, isExamplesTab?: boolean, noindex?: boolean}} ExamplesPageData
*/
// Initialize markdown parser and metadata service
@@ -33,6 +33,7 @@ export const data = {
// Generate URLs in the format /docs/elements/{component-name}/examples/ or package-specific component docs paths.
permalink: (/** @type {ExamplesPageData} */ data) => {
const filePath = data.component.filePathStem;
+ if (filePath.includes('/viewport/')) return `${data.component.url}examples/`;
let dir = 'elements';
if (filePath.includes('/code/')) dir = 'code';
else if (filePath.includes('/monaco/')) dir = 'monaco';
@@ -119,7 +120,7 @@ export async function render(data) {
const params = new URLSearchParams(window.location.search);
const cyclingExample = document.querySelector('#cycling-example');
const exampleSelector = document.querySelector('.example-selector');
- const exampleSelectorItems = Array.from(exampleSelector.querySelectorAll('nve-menu-item'));
+ const exampleSelectorItems = Array.from(exampleSelector?.querySelectorAll('nve-menu-item') ?? []);
const examples = ${JSON.stringify(exampleTemplates.map(i => ({ id: i.id, template: md.utils.escapeHtml(i.template) })))};
const exampleId = params.get('example') ?? examples[0].id;
const example = examples.find(e => e.id === exampleId);
@@ -131,10 +132,11 @@ export async function render(data) {
if (example) {
cyclingExample.source = unescapeHtml(example.template);
- exampleSelectorItems.find(item => item.value === exampleId).selected = true;
+ const selectedItem = exampleSelectorItems.find(item => item.value === exampleId);
+ if (selectedItem) selectedItem.selected = true;
}
- exampleSelector.addEventListener('click', (event) => {
+ exampleSelector?.addEventListener('click', (event) => {
const example = examples.find(e => e.id === event.target.value);
if (example) {
exampleSelectorItems.forEach(item => item.selected = false);
diff --git a/projects/site/src/docs/elements/viewport.md b/projects/site/src/docs/elements/viewport.md
deleted file mode 100644
index 597251d6aa..0000000000
--- a/projects/site/src/docs/elements/viewport.md
+++ /dev/null
@@ -1,53 +0,0 @@
----
-{
- title: 'Viewport',
- description: 'Use the NVIDIA Elements viewport to navigate arbitrary spatial content with pan, zoom, fitting, grid lines, and programmatic controls.',
- layout: 'docs.11ty.js',
- tag: 'nve-viewport',
- associatedElements: ['nve-viewport-gridlines', 'nve-viewport-minimap']
-}
----
-
-## Installation
-
-{% install 'nve-viewport' %}
-
-## Panning
-
-{% api 'nve-viewport', 'property', 'behaviorPan' %}
-
-## Zooming
-
-{% api 'nve-viewport', 'property', 'behaviorZoom' %}
-
-## Customizable Minimap
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'Minimap' '{ "inline": false, "height": "420px" }' %}
-
-## Interactive Content
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'InteractiveContent' '{ "inline": false, "height": "420px" }' %}
-
-## Panning with Space
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'PanWithSpace' '{ "inline": false, "height": "420px" }' %}
-
-## Invoker Commands
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'Commands' '{ "inline": false, "height": "420px" }' %}
-
-## Programmatic Navigation
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'ProgrammaticNavigation' '{ "inline": false, "height": "420px" }' %}
-
-## Dot Grid
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'Dots' '{ "inline": false, "height": "420px" }' %}
-
-## Cross Grid
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'Crosses' '{ "inline": false, "height": "420px" }' %}
-
-## Customizable Background
-
-{% example '@nvidia-elements/core/viewport/viewport.examples.json' 'Background' '{ "inline": false, "height": "420px" }' %}
diff --git a/projects/site/src/docs/viewport/gridlines.md b/projects/site/src/docs/viewport/gridlines.md
new file mode 100644
index 0000000000..ee1d590158
--- /dev/null
+++ b/projects/site/src/docs/viewport/gridlines.md
@@ -0,0 +1,26 @@
+---
+{
+ title: 'Gridlines',
+ description: 'Add lines, dots, or crosses behind NVIDIA Elements viewport content.',
+ layout: 'docs.11ty.js',
+ tag: 'nve-viewport-gridlines'
+}
+---
+
+## Installation
+
+{% install 'nve-viewport-gridlines' %}
+
+## Grid Spacing
+
+{% api 'nve-viewport-gridlines', 'property', 'step' %}
+
+{% api 'nve-viewport-gridlines', 'property', 'targetSpacing' %}
+
+## Dot Grid
+
+{% example '@nvidia-elements/viewport/gridlines/gridlines.examples.json' 'Dots' '{ "inline": false, "height": "420px" }' %}
+
+## Cross Grid
+
+{% example '@nvidia-elements/viewport/gridlines/gridlines.examples.json' 'Crosses' '{ "inline": false, "height": "420px" }' %}
diff --git a/projects/site/src/docs/viewport/index.md b/projects/site/src/docs/viewport/index.md
new file mode 100644
index 0000000000..02cb1367fb
--- /dev/null
+++ b/projects/site/src/docs/viewport/index.md
@@ -0,0 +1,40 @@
+---
+{
+ title: 'Viewport',
+ description: 'Use the NVIDIA Elements viewport to navigate spatial content with pan, zoom, fitting, and programmatic controls.',
+ layout: 'docs.11ty.js',
+ tag: 'nve-viewport'
+}
+---
+
+## Installation
+
+{% install 'nve-viewport' %}
+
+## Panning
+
+{% api 'nve-viewport', 'property', 'behaviorPan' %}
+
+## Zooming
+
+{% api 'nve-viewport', 'property', 'behaviorZoom' %}
+
+## Interactive Content
+
+{% example '@nvidia-elements/viewport/viewport/viewport.examples.json' 'InteractiveContent' '{ "inline": false, "height": "420px" }' %}
+
+## Panning with Space
+
+{% example '@nvidia-elements/viewport/viewport/viewport.examples.json' 'PanWithSpace' '{ "inline": false, "height": "420px" }' %}
+
+## Invoker Commands
+
+{% example '@nvidia-elements/viewport/viewport/viewport.examples.json' 'Commands' '{ "inline": false, "height": "420px" }' %}
+
+## Programmatic Navigation
+
+{% example '@nvidia-elements/viewport/viewport/viewport.examples.json' 'ProgrammaticNavigation' '{ "inline": false, "height": "420px" }' %}
+
+## Customizable Background
+
+{% example '@nvidia-elements/viewport/viewport/viewport.examples.json' 'Background' '{ "inline": false, "height": "420px" }' %}
diff --git a/projects/site/src/docs/viewport/minimap.md b/projects/site/src/docs/viewport/minimap.md
new file mode 100644
index 0000000000..427b05459b
--- /dev/null
+++ b/projects/site/src/docs/viewport/minimap.md
@@ -0,0 +1,16 @@
+---
+{
+ title: 'Minimap',
+ description: 'Show an overview of NVIDIA Elements viewport content and navigate its visible region.',
+ layout: 'docs.11ty.js',
+ tag: 'nve-viewport-minimap'
+}
+---
+
+## Installation
+
+{% install 'nve-viewport-minimap' %}
+
+## Custom Preview
+
+{% example '@nvidia-elements/viewport/minimap/minimap.examples.json' 'Default' '{ "inline": false, "height": "420px" }' %}
diff --git a/projects/site/src/docs/whats-new/09-2026.md b/projects/site/src/docs/whats-new/09-2026.md
index d9120d9a3d..69151c0fe8 100644
--- a/projects/site/src/docs/whats-new/09-2026.md
+++ b/projects/site/src/docs/whats-new/09-2026.md
@@ -25,7 +25,7 @@ September adds a pan-and-zoom surface for spatial interfaces, more flexible navi
### Navigate Spatial Content with Viewport
-The new [Viewport](/docs/elements/viewport/) gives diagrams, graphs, topology maps, and other canvas-style content a shared pan-and-zoom surface. Enable pointer, wheel, keyboard, and pinch navigation with `behavior-pan` and `behavior-zoom`, or set `behavior-pan="space"` to require holding Space before dragging. The `autofit` attribute frames content after the first layout, while `nve-viewport-gridlines` and `nve-viewport-minimap` help people keep their place. Toolbar buttons can control the viewport through Invoker commands such as `--zoom-in` and `--zoom-to-fit`. Viewport is not yet marked stable, so expect API refinements.
+The new [Viewport](/docs/viewport/) gives diagrams, graphs, topology maps, and other canvas-style content a shared pan-and-zoom surface. Enable pointer, wheel, keyboard, and pinch navigation with `behavior-pan` and `behavior-zoom`, or set `behavior-pan="space"` to require holding Space before dragging. The `autofit` attribute frames content after the first layout, while `nve-viewport-gridlines` and `nve-viewport-minimap` help people keep their place. Toolbar buttons can control the viewport through Invoker commands such as `--zoom-in` and `--zoom-to-fit`. Viewport is not yet marked stable, so expect API refinements.

diff --git a/projects/viewport/.gitignore b/projects/viewport/.gitignore
new file mode 100644
index 0000000000..5f030ac5b4
--- /dev/null
+++ b/projects/viewport/.gitignore
@@ -0,0 +1,4 @@
+dist/
+node_modules/
+coverage/
+.DS_Store
diff --git a/projects/viewport/.visual/gridlines-crosses.dark.png b/projects/viewport/.visual/gridlines-crosses.dark.png
new file mode 100644
index 0000000000..1c97faafca
--- /dev/null
+++ b/projects/viewport/.visual/gridlines-crosses.dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4d36575bde607a5592d2374262cb759df1e74a9e8137386b2372f42cf437a9f9
+size 4158
diff --git a/projects/viewport/.visual/gridlines-crosses.png b/projects/viewport/.visual/gridlines-crosses.png
new file mode 100644
index 0000000000..30cdb56f59
--- /dev/null
+++ b/projects/viewport/.visual/gridlines-crosses.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:48ba4563c892e62e59a0ba90178e5ad7d48579ecf45ff530d1ae41ea4f4ed603
+size 4128
diff --git a/projects/viewport/.visual/gridlines-dots.dark.png b/projects/viewport/.visual/gridlines-dots.dark.png
new file mode 100644
index 0000000000..f1e3fe2f7c
--- /dev/null
+++ b/projects/viewport/.visual/gridlines-dots.dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cc585ed2215ae44d39fe0c46ded50e10b0ff32697be595eeea448afd75a5125e
+size 4220
diff --git a/projects/viewport/.visual/gridlines-dots.png b/projects/viewport/.visual/gridlines-dots.png
new file mode 100644
index 0000000000..c82c8bb56c
--- /dev/null
+++ b/projects/viewport/.visual/gridlines-dots.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f631073773047ad2195db77829bd110b5aa34f00136a2c893e7ee01eca0a3f06
+size 4204
diff --git a/projects/viewport/.visual/gridlines.dark.png b/projects/viewport/.visual/gridlines.dark.png
new file mode 100644
index 0000000000..d377d70852
--- /dev/null
+++ b/projects/viewport/.visual/gridlines.dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:77d918a32b6c3f4798f67f87825ce605eb0996a133d0ec4748178ef35beb0e2c
+size 3923
diff --git a/projects/viewport/.visual/gridlines.png b/projects/viewport/.visual/gridlines.png
new file mode 100644
index 0000000000..68b6747d65
--- /dev/null
+++ b/projects/viewport/.visual/gridlines.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2ec9c73de0381d5d24a8c77a7f1ee409e9942beb034062d61a3fe524b7e63324
+size 3900
diff --git a/projects/viewport/.visual/minimap-custom.dark.png b/projects/viewport/.visual/minimap-custom.dark.png
new file mode 100644
index 0000000000..55d5fd1495
--- /dev/null
+++ b/projects/viewport/.visual/minimap-custom.dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27621a3e520ecec474af02ae0bfb7ab0a1883146bcf4fb4eb37d0cd07588cd5b
+size 5358
diff --git a/projects/viewport/.visual/minimap-custom.png b/projects/viewport/.visual/minimap-custom.png
new file mode 100644
index 0000000000..9bb76e300c
--- /dev/null
+++ b/projects/viewport/.visual/minimap-custom.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1adcc311765fc0c5de4930861d217d732eeba131571cb5bbd72a293df694ee9f
+size 5456
diff --git a/projects/viewport/.visual/minimap.dark.png b/projects/viewport/.visual/minimap.dark.png
new file mode 100644
index 0000000000..e1266870dc
--- /dev/null
+++ b/projects/viewport/.visual/minimap.dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2425f795fdb8b6a3db84dc1dd28ca69b85539f1a6f40148614f76a83a12f5f01
+size 4763
diff --git a/projects/viewport/.visual/minimap.png b/projects/viewport/.visual/minimap.png
new file mode 100644
index 0000000000..2cc56375e4
--- /dev/null
+++ b/projects/viewport/.visual/minimap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:22137b71d9ea2ff5d3d8da16784752b97262a7d6d3ff73d871d1f82fbe2a39e5
+size 4818
diff --git a/projects/viewport/.visual/viewport.dark.png b/projects/viewport/.visual/viewport.dark.png
new file mode 100644
index 0000000000..77c682aa08
--- /dev/null
+++ b/projects/viewport/.visual/viewport.dark.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd9b845cbcff343ea947d3fb42729bc6c36e2210f9be3d29b1784e0f787606c6
+size 3498
diff --git a/projects/viewport/.visual/viewport.png b/projects/viewport/.visual/viewport.png
new file mode 100644
index 0000000000..b7089e1d4b
--- /dev/null
+++ b/projects/viewport/.visual/viewport.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1702da5f0893c3d67df34dad4cf9741151d759b1956806684fc2f6b220459b43
+size 3503
diff --git a/projects/viewport/CHANGELOG.md b/projects/viewport/CHANGELOG.md
new file mode 100644
index 0000000000..98eff1b99e
--- /dev/null
+++ b/projects/viewport/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.0
+
+Initial release.
diff --git a/projects/viewport/DEVELOPMENT.md b/projects/viewport/DEVELOPMENT.md
new file mode 100644
index 0000000000..fbe4901d53
--- /dev/null
+++ b/projects/viewport/DEVELOPMENT.md
@@ -0,0 +1,18 @@
+# Development
+
+| Command | Description |
+| -------------------------- | ----------------------------------------- |
+| `pnpm run build` | Build the library |
+| `pnpm run build:watch` | Build the library with file watching |
+| `pnpm run dev` | Start development mode with file watching |
+| `pnpm run lint` | Lint source files |
+| `pnpm run lint:fix` | Fix source lint issues |
+| `pnpm run test` | Run unit tests |
+| `pnpm run test:watch` | Run unit tests in watch mode |
+| `pnpm run test:axe` | Run accessibility tests |
+| `pnpm run test:coverage` | Run unit tests with coverage |
+| `pnpm run test:types` | Type-check source, tests, and examples |
+| `pnpm run test:visual` | Run visual regression tests |
+| `pnpm run test:ssr` | Run server-side rendering tests |
+| `pnpm run test:lighthouse` | Run Lighthouse performance tests |
+| `pnpm run ci` | Run the project CI pipeline |
diff --git a/projects/viewport/NOTICE.md b/projects/viewport/NOTICE.md
new file mode 100644
index 0000000000..2dca287339
--- /dev/null
+++ b/projects/viewport/NOTICE.md
@@ -0,0 +1,44 @@
+NOTICE
+
+Elements - NVIDIA Elements Design System
+Copyright 2024-2026 NVIDIA Corporation
+
+This project includes the following bundled third-party software:
+
+- lit v3.3.3 [BSD-3-Clause]
+ Copyright: Google LLC
+
+==============================================================================
+BSD-3-Clause
+==============================================================================
+
+The following bundled components are provided under the BSD-3-Clause license:
+
+lit v3.3.3 - Copyright Google LLC
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+For license details, see: BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause
diff --git a/projects/viewport/README.md b/projects/viewport/README.md
new file mode 100644
index 0000000000..86d81652a4
--- /dev/null
+++ b/projects/viewport/README.md
@@ -0,0 +1,89 @@
+# @nvidia-elements/viewport
+
+NVIDIA Design System and UI Agent Harness for AI/ML Factories, Robotics, and Autonomous Vehicles.
+
+Web Components for navigating spatial content with pan, zoom, content fitting, background grids, and a minimap.
+
+- [Documentation](https://NVIDIA.github.io/elements/docs/viewport/)
+- [Changelog](https://NVIDIA.github.io/elements/docs/changelog/)
+- [GitHub Repo](https://github.com/NVIDIA/elements)
+- [npm](https://www.npmjs.com/package/@nvidia-elements/viewport)
+
+## Components
+
+| Component | Tag | Description |
+| ----------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------------------- |
+| [Viewport](https://NVIDIA.github.io/elements/docs/viewport/) | `nve-viewport` | A spatial surface with optional pan, zoom, and content fitting. |
+| [Gridlines](https://NVIDIA.github.io/elements/docs/viewport/gridlines/) | `nve-viewport-gridlines` | A background grid with lines, dots, or crosses that adapts to zoom. |
+| [Minimap](https://NVIDIA.github.io/elements/docs/viewport/minimap/) | `nve-viewport-minimap` | A content overview with a visible region indicator and direct navigation. |
+
+## Getting Started
+
+Install the package and its peer dependencies:
+
+```shell
+npm install @nvidia-elements/viewport @nvidia-elements/core @nvidia-elements/themes
+```
+
+Load the base theme in your application's stylesheet:
+
+```css
+@import '@nvidia-elements/themes/index.css';
+```
+
+## Usage
+
+Register each component your application uses through its own entrypoint:
+
+```javascript
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/gridlines/define.js';
+import '@nvidia-elements/viewport/minimap/define.js';
+```
+
+```html
+
+
+
+
+
+
+
+```
+
+Place spatial content in the default slot. Gridlines and Minimap must be direct children of the viewport; they use the `background` and `overlay` slots automatically.
+
+`behavior-pan` and `behavior-zoom` enable user navigation. Use `behavior-pan="space"` to require Space for primary pointer panning when content handles its own drag interactions. `autofit` frames the initial content, and `fit-inset` controls the space around it.
+
+Gridlines supports `lines`, `dots`, and `crosses` patterns. Minimap draws rectangles for the viewport's content elements; provide content in its `preview` slot to replace them with a custom preview.
+
+## Programmatic Navigation
+
+Use `reveal()` to frame a content region and `fitContents()` to frame all content:
+
+```javascript
+const viewport = document.querySelector('nve-viewport');
+
+viewport.reveal({ x: 0, y: 0, width: 900, height: 600 }, { animated: true });
+viewport.fitContents({ animated: true });
+```
+
+Listen for `viewportchange` to observe committed transform changes. The event detail contains the current `x`, `y`, and `scale` values.
+
+## NVIDIA Elements Skill
+
+Install the Elements agent skill with the open [skills](https://www.skills.sh/nvidia/elements/elements) CLI:
+
+```shell
+npx skills add https://github.com/nvidia/elements --skill elements
+```
diff --git a/projects/viewport/eslint.config.js b/projects/viewport/eslint.config.js
new file mode 100644
index 0000000000..ee96dbce97
--- /dev/null
+++ b/projects/viewport/eslint.config.js
@@ -0,0 +1,5 @@
+import { elementsRecommended } from '@nvidia-elements/lint/eslint';
+import { browserTypescriptConfig, libraryConfig, litConfig, jsonConfig } from '@internals/eslint';
+
+/** @type {import('eslint').Linter.Config[]} */
+export default [...elementsRecommended, ...browserTypescriptConfig, ...libraryConfig, ...litConfig, ...jsonConfig];
diff --git a/projects/viewport/package.json b/projects/viewport/package.json
new file mode 100644
index 0000000000..2afb08431f
--- /dev/null
+++ b/projects/viewport/package.json
@@ -0,0 +1,391 @@
+{
+ "name": "@nvidia-elements/viewport",
+ "version": "0.0.0",
+ "release": {
+ "extends": "../../release.config.js"
+ },
+ "description": "A viewport for spatial content with pan and zoom navigation.",
+ "keywords": [
+ "nvidia",
+ "web-components",
+ "custom-elements",
+ "ui-components",
+ "design-system"
+ ],
+ "type": "module",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NVIDIA/elements.git"
+ },
+ "customElements": "dist/custom-elements.json",
+ "contributes": {
+ "html": {
+ "customData": [
+ "./dist/data.html.json"
+ ]
+ },
+ "snippets": [
+ {
+ "language": "html",
+ "path": "./dist/data.snippets.json"
+ }
+ ]
+ },
+ "files": [
+ "README.md",
+ "CHANGELOG.md",
+ "NOTICE.md",
+ "package.json",
+ "dist/**/*"
+ ],
+ "exports": {
+ "./package.json": "./package.json",
+ "./custom-elements.json": "./dist/custom-elements.json",
+ "./data.html.json": "./dist/data.html.json",
+ "./*.examples.json": "./dist/*.examples.json",
+ ".": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./index.js": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "./gridlines": {
+ "types": "./dist/gridlines/index.d.ts",
+ "default": "./dist/gridlines/index.js"
+ },
+ "./gridlines/define.js": {
+ "types": "./dist/gridlines/define.d.ts",
+ "default": "./dist/gridlines/define.js"
+ },
+ "./minimap": {
+ "types": "./dist/minimap/index.d.ts",
+ "default": "./dist/minimap/index.js"
+ },
+ "./minimap/define.js": {
+ "types": "./dist/minimap/define.d.ts",
+ "default": "./dist/minimap/define.js"
+ },
+ "./viewport": {
+ "types": "./dist/viewport/index.d.ts",
+ "default": "./dist/viewport/index.js"
+ },
+ "./viewport/define.js": {
+ "types": "./dist/viewport/define.d.ts",
+ "default": "./dist/viewport/define.js"
+ }
+ },
+ "sideEffects": [
+ "./src/**/define.ts",
+ "./**/define.js"
+ ],
+ "scripts": {
+ "ci": "wireit",
+ "dev": "wireit",
+ "build": "wireit",
+ "build:watch": "wireit",
+ "lint": "wireit",
+ "lint:fix": "wireit",
+ "test": "wireit",
+ "test:axe": "wireit",
+ "test:coverage": "wireit",
+ "test:types": "wireit",
+ "test:ssr": "wireit",
+ "test:visual": "wireit",
+ "test:watch": "wireit",
+ "test:lighthouse": "wireit"
+ },
+ "dependencies": {
+ "lit": "catalog:publish"
+ },
+ "peerDependencies": {
+ "@nvidia-elements/core": "workspace:^",
+ "@nvidia-elements/themes": "workspace:^"
+ },
+ "devDependencies": {
+ "@eslint/js": "catalog:",
+ "@internals/eslint": "workspace:*",
+ "@internals/testing": "workspace:*",
+ "@internals/vite": "workspace:*",
+ "@nvidia-elements/lint": "workspace:*",
+ "@nvidia-elements/styles": "workspace:*",
+ "@vitest/browser": "catalog:",
+ "@vitest/coverage-istanbul": "catalog:",
+ "axe-core": "catalog:",
+ "eslint": "catalog:",
+ "lit": "catalog:",
+ "publint": "catalog:",
+ "stylelint": "catalog:",
+ "stylelint-config-standard": "catalog:",
+ "typescript": "catalog:",
+ "typescript-7": "catalog:",
+ "vite": "catalog:",
+ "vitest": "catalog:"
+ },
+ "wireit": {
+ "dev": {
+ "command": "NODE_ENV=development vite build --watch",
+ "service": true,
+ "dependencies": [
+ "../core:build",
+ "../internals/vite:ci"
+ ]
+ },
+ "ci": {
+ "dependencies": [
+ "build",
+ "lint",
+ "publint",
+ "test:axe",
+ "test:ssr",
+ "test:coverage",
+ "test:types",
+ "test:visual"
+ ]
+ },
+ "publint": {
+ "command": "publint",
+ "dependencies": [
+ "build"
+ ],
+ "files": [
+ "package.json"
+ ],
+ "output": []
+ },
+ "build": {
+ "command": "NODE_ENV=production vite build",
+ "files": [
+ "src/**",
+ "!src/**/*.test.ts",
+ "!src/**/*.test.lighthouse.ts",
+ "!src/**/*.test.axe.ts",
+ "!src/**/*.test.ssr.ts",
+ "!src/**/__screenshots__/**",
+ "package.json",
+ "tsconfig.json",
+ "tsconfig.lib.json",
+ "vite.config.ts"
+ ],
+ "output": [
+ "dist/**"
+ ],
+ "dependencies": [
+ "../internals/vite:ci",
+ {
+ "script": "../themes:build",
+ "cascade": false
+ },
+ {
+ "script": "../core:build",
+ "cascade": false
+ }
+ ]
+ },
+ "build:watch": {
+ "command": "vite build --watch",
+ "service": true,
+ "dependencies": [
+ "build"
+ ]
+ },
+ "test": {
+ "command": "vitest run",
+ "files": [
+ "src/**",
+ "!src/**/*.test.axe.ts",
+ "!src/**/*.test.lighthouse.ts",
+ "!src/**/*.test.ssr.ts",
+ "!src/**/*.test.visual.ts",
+ "!src/**/__screenshots__/**",
+ "tsconfig.json",
+ "vite.config.ts",
+ "vitest.config.ts"
+ ],
+ "output": [],
+ "dependencies": [
+ "build",
+ "../internals/testing:build",
+ "../internals/vite:ci",
+ "../core:build"
+ ]
+ },
+ "test:coverage": {
+ "command": "vitest run --coverage",
+ "files": [
+ "src/**",
+ "!src/**/*.test.axe.ts",
+ "!src/**/*.test.lighthouse.ts",
+ "!src/**/*.test.ssr.ts",
+ "!src/**/*.test.visual.ts",
+ "!src/**/__screenshots__/**",
+ "tsconfig.json",
+ "vite.config.ts",
+ "vitest.config.ts"
+ ],
+ "output": [
+ "coverage/unit/**"
+ ],
+ "dependencies": [
+ "build",
+ "../internals/testing:build",
+ "../internals/vite:ci",
+ "../core:build"
+ ]
+ },
+ "test:types": {
+ "command": "node ./node_modules/typescript-7/bin/tsc --project tsconfig.json --noEmit",
+ "files": [
+ "src/**/*.ts",
+ "tsconfig.json",
+ "../core/dist/**/*.d.ts",
+ "../forms/dist/**/*.d.ts",
+ "../internals/testing/dist/**/*.d.ts",
+ "../internals/vite/src/**/*.d.ts"
+ ],
+ "output": [],
+ "dependencies": [
+ {
+ "script": "../core:build",
+ "cascade": false
+ },
+ {
+ "script": "../internals/testing:build",
+ "cascade": false
+ }
+ ]
+ },
+ "test:watch": {
+ "command": "vitest --watch",
+ "service": true
+ },
+ "test:lighthouse": {
+ "command": "playwright-lock 'vitest run --config=vitest.lighthouse.ts'",
+ "files": [
+ "src/index.test.lighthouse.ts",
+ "src/**/*.test.lighthouse.ts",
+ "vitest.lighthouse.html",
+ "vitest.lighthouse.ts"
+ ],
+ "output": [
+ ".lighthouse"
+ ],
+ "dependencies": [
+ "build",
+ "../internals/vite:ci"
+ ],
+ "env": {
+ "NODE_ENV": "production"
+ }
+ },
+ "test:visual": {
+ "command": "vitest run --config=vitest.visual.ts",
+ "clean": false,
+ "files": [
+ "dist/**/*.js",
+ "src/**/*.test.visual.ts",
+ "vitest.visual.html",
+ "vitest.visual.ts"
+ ],
+ "output": [
+ ".visual/*.png",
+ "coverage/visual/**"
+ ],
+ "dependencies": [
+ "../internals/vite:ci",
+ {
+ "script": "build",
+ "cascade": false
+ }
+ ]
+ },
+ "test:axe": {
+ "command": "vitest run --config=vitest.axe.ts",
+ "files": [
+ "dist/**/*.js",
+ "src/**/*.test.axe.ts",
+ "tsconfig.json",
+ "vitest.axe.ts"
+ ],
+ "output": [
+ "coverage/axe/**"
+ ],
+ "dependencies": [
+ "../internals/testing:build",
+ "../internals/vite:ci",
+ {
+ "script": "build",
+ "cascade": false
+ }
+ ],
+ "env": {
+ "NODE_ENV": "production"
+ }
+ },
+ "test:ssr": {
+ "command": "vitest run --config=vitest.ssr.ts",
+ "files": [
+ "dist/**/*.js",
+ "src/**/*.test.ssr.ts",
+ "tsconfig.json",
+ "vitest.ssr.ts",
+ "../themes/dist/**/*.css"
+ ],
+ "output": [
+ "coverage/ssr/**"
+ ],
+ "dependencies": [
+ "../internals/vite:ci",
+ {
+ "script": "build",
+ "cascade": false
+ },
+ {
+ "script": "../themes:build",
+ "cascade": false
+ }
+ ],
+ "env": {
+ "NODE_ENV": "production"
+ }
+ },
+ "lint": {
+ "dependencies": [
+ "lint:eslint",
+ "lint:style"
+ ]
+ },
+ "lint:eslint": {
+ "command": "eslint -c ./eslint.config.js --color --cache --cache-location .eslintcache/",
+ "files": [
+ "src/**/*.ts",
+ "eslint.config.js"
+ ],
+ "output": [],
+ "dependencies": [
+ "../internals/eslint:ci",
+ "../lint:build"
+ ]
+ },
+ "lint:style": {
+ "command": "stylelint 'src/**/*.css' --config=../../stylelint.config.mjs",
+ "files": [
+ "src/**/*.css",
+ "../../stylelint.config.mjs"
+ ],
+ "output": []
+ },
+ "lint:fix": {
+ "command": "eslint -c ./eslint.config.js --fix",
+ "dependencies": [
+ "../internals/eslint:ci",
+ "../lint:build"
+ ]
+ }
+ },
+ "publishConfig": {
+ "provenance": true
+ },
+ "license": "Apache-2.0"
+}
diff --git a/projects/viewport/src/declarations.d.ts b/projects/viewport/src/declarations.d.ts
new file mode 100644
index 0000000000..5db409c190
--- /dev/null
+++ b/projects/viewport/src/declarations.d.ts
@@ -0,0 +1,12 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+declare module '*.css' {
+ const content: string;
+ export default content;
+}
+
+declare module '*.css?inline' {
+ const content: string;
+ export = content;
+}
diff --git a/projects/core/src/viewport/define.ts b/projects/viewport/src/gridlines/define.ts
similarity index 61%
rename from projects/core/src/viewport/define.ts
rename to projects/viewport/src/gridlines/define.ts
index 937b2158a7..2218eed0e4 100644
--- a/projects/core/src/viewport/define.ts
+++ b/projects/viewport/src/gridlines/define.ts
@@ -2,16 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
import { define } from '@nvidia-elements/core/internal';
-import { Viewport, ViewportGridlines, ViewportMinimap } from '@nvidia-elements/core/viewport';
+import { ViewportGridlines } from '@nvidia-elements/viewport/gridlines';
+import '@nvidia-elements/viewport/viewport/define.js';
-define(Viewport);
define(ViewportGridlines);
-define(ViewportMinimap);
declare global {
interface HTMLElementTagNameMap {
- 'nve-viewport': Viewport;
'nve-viewport-gridlines': ViewportGridlines;
- 'nve-viewport-minimap': ViewportMinimap;
}
}
diff --git a/projects/viewport/src/gridlines/gridlines-registration.test.ts b/projects/viewport/src/gridlines/gridlines-registration.test.ts
new file mode 100644
index 0000000000..36c4187194
--- /dev/null
+++ b/projects/viewport/src/gridlines/gridlines-registration.test.ts
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
+
+describe('gridlines registration', () => {
+ let fixture: HTMLElement;
+
+ beforeEach(async () => {
+ fixture = await createFixture();
+ });
+
+ afterEach(() => {
+ removeFixture(fixture);
+ });
+
+ it('renders existing markup when the gridlines entrypoint loads before the viewport entrypoint', async () => {
+ fixture.innerHTML = `
+
+
+
+ `;
+ expect(customElements.get('nve-viewport')).toBeUndefined();
+
+ await import(/* @vite-ignore */ new URL('../../dist/gridlines/define.js', import.meta.url).href);
+
+ expect(customElements.get('nve-viewport')).toBeDefined();
+ expect(customElements.get('nve-viewport-minimap')).toBeUndefined();
+ const viewport = fixture.querySelector('nve-viewport');
+ const gridlines = fixture.querySelector('nve-viewport-gridlines');
+ if (!viewport || !gridlines) {
+ throw new Error('Viewport and gridlines are missing.');
+ }
+ await elementIsStable(viewport);
+ await elementIsStable(gridlines);
+ await expect
+ .poll(() => gridlines.shadowRoot?.querySelector('[data-gridline-pattern]'))
+ .toBeInstanceOf(SVGPatternElement);
+
+ viewport.scale = 2;
+ await elementIsStable(viewport);
+ expect(gridlines.style.getPropertyValue('--_scale')).toBe('2');
+ });
+});
diff --git a/projects/core/src/viewport/viewport-gridlines.css b/projects/viewport/src/gridlines/gridlines.css
similarity index 100%
rename from projects/core/src/viewport/viewport-gridlines.css
rename to projects/viewport/src/gridlines/gridlines.css
diff --git a/projects/viewport/src/gridlines/gridlines.examples.ts b/projects/viewport/src/gridlines/gridlines.examples.ts
new file mode 100644
index 0000000000..02b66ea24e
--- /dev/null
+++ b/projects/viewport/src/gridlines/gridlines.examples.ts
@@ -0,0 +1,86 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/gridlines/define.js';
+
+export default {
+ title: 'Elements/Viewport/Gridlines',
+ component: 'nve-viewport-gridlines'
+};
+
+/**
+ * @summary Use grid lines as spatial reference points while navigating viewport content.
+ */
+export const Default = {
+ render: () => html`
+
+
+
+
+
+ `
+};
+
+/**
+ * @summary Optionally configure viewport grid lines to use dots instead of lines.
+ */
+export const Dots = {
+ render: () => html`
+
+
+
+
+
+ `
+};
+
+/**
+ * @summary Use crosses as spatial reference points to help users judge positions while keeping the content between them unobstructed.
+ */
+export const Crosses = {
+ render: () => html`
+
+
+
+
+
+ `
+};
diff --git a/projects/viewport/src/gridlines/gridlines.test.axe.ts b/projects/viewport/src/gridlines/gridlines.test.axe.ts
new file mode 100644
index 0000000000..6f5f0748b6
--- /dev/null
+++ b/projects/viewport/src/gridlines/gridlines.test.axe.ts
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import { afterEach, describe, expect, it } from 'vitest';
+import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
+import { runAxe } from '@internals/testing/axe';
+import { ViewportGridlines } from '@nvidia-elements/viewport/gridlines';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/gridlines/define.js';
+
+describe(ViewportGridlines.metadata.tag, () => {
+ let fixture: HTMLElement;
+
+ afterEach(() => removeFixture(fixture));
+
+ it.each(['lines', 'dots', 'crosses'] as const)('should pass axe check with %s', async pattern => {
+ fixture = await createFixture(html`
+
+
+
+
+ `);
+ const viewport = fixture.querySelector('nve-viewport');
+ const gridlines = fixture.querySelector('nve-viewport-gridlines');
+ if (!viewport || !gridlines) throw new Error('Gridlines fixture is missing.');
+ await elementIsStable(viewport);
+ await elementIsStable(gridlines);
+
+ const results = await runAxe(['nve-viewport']);
+ expect(results.violations.length).toBe(0);
+ });
+});
diff --git a/projects/viewport/src/gridlines/gridlines.test.lighthouse.ts b/projects/viewport/src/gridlines/gridlines.test.lighthouse.ts
new file mode 100644
index 0000000000..45f7965b9b
--- /dev/null
+++ b/projects/viewport/src/gridlines/gridlines.test.lighthouse.ts
@@ -0,0 +1,28 @@
+// 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('gridlines lighthouse report', () => {
+ test('gridlines should meet lighthouse benchmarks', async () => {
+ const report = await lighthouseRunner.getReport(
+ 'nve-viewport-gridlines',
+ /* html */ `
+
+
+ content
+
+
+ `
+ );
+
+ expect(report.scores.performance).toBe(100);
+ expect(report.scores.accessibility).toBe(100);
+ expect(report.scores.bestPractices).toBe(100);
+ expect(report.payload.javascript.kb).toBeLessThan(25.1);
+ });
+});
diff --git a/projects/viewport/src/gridlines/gridlines.test.ssr.ts b/projects/viewport/src/gridlines/gridlines.test.ssr.ts
new file mode 100644
index 0000000000..0bdd40d573
--- /dev/null
+++ b/projects/viewport/src/gridlines/gridlines.test.ssr.ts
@@ -0,0 +1,26 @@
+// 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 { ViewportGridlines } from '@nvidia-elements/viewport/gridlines';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/gridlines/define.js';
+
+describe(ViewportGridlines.metadata.tag, () => {
+ it('should render in the background slot on the server', async () => {
+ const result = await ssrRunner.render(html`
+
+
+ content
+
+ `);
+
+ const tag = result.match(/]*>/)?.[0];
+ expect(tag).toBeDefined();
+ expect(tag).toContain('slot="background"');
+ expect(tag).toContain('aria-hidden="true"');
+ expect(result.match(/shadowroot="open"/g)).toHaveLength(2);
+ });
+});
diff --git a/projects/core/src/viewport/viewport-gridlines.test.ts b/projects/viewport/src/gridlines/gridlines.test.ts
similarity index 95%
rename from projects/core/src/viewport/viewport-gridlines.test.ts
rename to projects/viewport/src/gridlines/gridlines.test.ts
index 2d40b2d24f..058ec749ed 100644
--- a/projects/core/src/viewport/viewport-gridlines.test.ts
+++ b/projects/viewport/src/gridlines/gridlines.test.ts
@@ -4,9 +4,11 @@
import { html } from 'lit';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
-import { Viewport, ViewportGridlines } from '@nvidia-elements/core/viewport';
-import { selectGridlineInterval } from './viewport-gridlines.utils.js';
-import '@nvidia-elements/core/viewport/define.js';
+import type { Viewport } from '@nvidia-elements/viewport/viewport';
+import { ViewportGridlines } from '@nvidia-elements/viewport/gridlines';
+import { selectGridlineInterval } from './gridlines.utils.js';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/gridlines/define.js';
class TestResizeObserver implements ResizeObserver {
static readonly instances: TestResizeObserver[] = [];
@@ -36,8 +38,11 @@ describe(ViewportGridlines.metadata.tag, () => {
`);
- viewport = fixture.querySelector(Viewport.metadata.tag);
- gridlines = fixture.querySelector(ViewportGridlines.metadata.tag);
+ const viewportElement = fixture.querySelector('nve-viewport');
+ const gridlinesElement = fixture.querySelector('nve-viewport-gridlines');
+ if (!viewportElement || !gridlinesElement) throw new Error('Viewport fixture is missing.');
+ viewport = viewportElement;
+ gridlines = gridlinesElement;
await elementIsStable(viewport);
await elementIsStable(gridlines);
});
@@ -48,6 +53,10 @@ describe(ViewportGridlines.metadata.tag, () => {
vi.unstubAllGlobals();
});
+ it('registers without unrelated viewport components', () => {
+ expect(customElements.get('nve-viewport-minimap')).toBeUndefined();
+ });
+
it('defines reflected properties with normalized defaults', async () => {
expect(customElements.get(ViewportGridlines.metadata.tag)).toBe(ViewportGridlines);
expect({
diff --git a/projects/viewport/src/gridlines/gridlines.test.visual.ts b/projects/viewport/src/gridlines/gridlines.test.visual.ts
new file mode 100644
index 0000000000..ddfbdf0deb
--- /dev/null
+++ b/projects/viewport/src/gridlines/gridlines.test.visual.ts
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { describe, expect, test } from 'vitest';
+import { visualRunner } from '@internals/vite';
+
+describe('gridlines visual', () => {
+ test('gridlines should match visual baseline', async () => {
+ const report = await visualRunner.render('gridlines', template());
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+
+ test('gridlines should match visual baseline dark theme', async () => {
+ const report = await visualRunner.render('gridlines.dark', template('dark'));
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+
+ test('gridlines dots should match visual baseline', async () => {
+ const report = await visualRunner.render('gridlines-dots', template('', 'dots'));
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+
+ test('gridlines dots should match visual baseline dark theme', async () => {
+ const report = await visualRunner.render('gridlines-dots.dark', template('dark', 'dots'));
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+
+ test('gridlines crosses should match visual baseline', async () => {
+ const report = await visualRunner.render('gridlines-crosses', template('', 'crosses'));
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+
+ test('gridlines crosses should match visual baseline dark theme', async () => {
+ const report = await visualRunner.render('gridlines-crosses.dark', template('dark', 'crosses'));
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+});
+
+function template(theme: '' | 'dark' = '', pattern: 'lines' | 'dots' | 'crosses' = 'lines') {
+ return /* html */ `
+
+
+
+
+ •︎•︎•︎•︎•︎
+ •︎•︎•︎
+
+
+ `;
+}
diff --git a/projects/core/src/viewport/viewport-gridlines.ts b/projects/viewport/src/gridlines/gridlines.ts
similarity index 95%
rename from projects/core/src/viewport/viewport-gridlines.ts
rename to projects/viewport/src/gridlines/gridlines.ts
index 8a2f264c40..c1e951a113 100644
--- a/projects/core/src/viewport/viewport-gridlines.ts
+++ b/projects/viewport/src/gridlines/gridlines.ts
@@ -5,14 +5,10 @@ import { html, LitElement, nothing, svg, type PropertyValues } from 'lit';
import { property } from 'lit/decorators/property.js';
import { styleMap } from 'lit/directives/style-map.js';
import { finiteOr, hostAttr, positiveFiniteOr, useStyles } from '@nvidia-elements/core/internal';
-import styles from './viewport-gridlines.css?inline';
-import {
- createGridlineCoverage,
- gridlineCoverageNeedsUpdate,
- selectGridlineInterval
-} from './viewport-gridlines.utils.js';
-import { Viewport } from './viewport.js';
-import type { ViewportRect } from './viewport.types.js';
+import styles from './gridlines.css?inline';
+import { createGridlineCoverage, gridlineCoverageNeedsUpdate, selectGridlineInterval } from './gridlines.utils.js';
+import { Viewport } from '@nvidia-elements/viewport/viewport';
+import type { ViewportRect } from '@nvidia-elements/viewport/viewport';
const DEFAULT_STEP = 10;
const DEFAULT_TARGET_SPACING = 64;
@@ -30,9 +26,9 @@ interface ProjectionSyncOptions {
/**
* @element nve-viewport-gridlines
* @description Renders origin-stable, zoom-adaptive gridline patterns behind viewport content.
- * @documentation https://nvidia.github.io/elements/docs/elements/viewport/
+ * @documentation https://nvidia.github.io/elements/docs/viewport/gridlines/
* @since 0.0.0
- * @entrypoint \@nvidia-elements/core/viewport
+ * @entrypoint \@nvidia-elements/viewport/gridlines
* @cssprop --color - Sets the gridline pattern color.
* @cssprop --line-width - Sets the approximate stroke width in CSS pixels.
* @cssprop --dot-radius - Sets the approximate dot radius in CSS pixels.
diff --git a/projects/core/src/viewport/viewport-gridlines.utils.ts b/projects/viewport/src/gridlines/gridlines.utils.ts
similarity index 97%
rename from projects/core/src/viewport/viewport-gridlines.utils.ts
rename to projects/viewport/src/gridlines/gridlines.utils.ts
index 9c28f6e5a4..51acf8976c 100644
--- a/projects/core/src/viewport/viewport-gridlines.utils.ts
+++ b/projects/viewport/src/gridlines/gridlines.utils.ts
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
-import type { ViewportRect } from './viewport.types.js';
+import type { ViewportRect } from '@nvidia-elements/viewport/viewport';
const MIN_RETAINED_SPACING_FACTOR = 0.5;
const MAX_RETAINED_SPACING_FACTOR = 1.5;
diff --git a/projects/viewport/src/gridlines/index.ts b/projects/viewport/src/gridlines/index.ts
new file mode 100644
index 0000000000..3e9ad65d54
--- /dev/null
+++ b/projects/viewport/src/gridlines/index.ts
@@ -0,0 +1,4 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from './gridlines.js';
diff --git a/projects/viewport/src/index.test.lighthouse.ts b/projects/viewport/src/index.test.lighthouse.ts
new file mode 100644
index 0000000000..9586b2f6a2
--- /dev/null
+++ b/projects/viewport/src/index.test.lighthouse.ts
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { expect, test, describe } from 'vitest';
+import { lighthouseRunner } from '@internals/vite';
+
+describe('lighthouse report', () => {
+ test('@nvidia-elements/viewport JS Bundles should remain within compressed bundle limits', async () => {
+ const report = await lighthouseRunner.getReport('bundles', /* html */`
+
+ `);
+
+ expect(report.payload.javascript.kb).toBeLessThan(2);
+ });
+});
diff --git a/projects/viewport/src/index.test.ts b/projects/viewport/src/index.test.ts
new file mode 100644
index 0000000000..e6faa980d7
--- /dev/null
+++ b/projects/viewport/src/index.test.ts
@@ -0,0 +1,11 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { describe, it, expect } from 'vitest';
+import { VERSION } from './index.js';
+
+describe('@nvidia-elements/viewport', () => {
+ it('should export VERSION', () => {
+ expect(VERSION).toBe('0.0.0');
+ });
+});
diff --git a/projects/viewport/src/index.ts b/projects/viewport/src/index.ts
new file mode 100644
index 0000000000..f8f476a942
--- /dev/null
+++ b/projects/viewport/src/index.ts
@@ -0,0 +1,4 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export const VERSION = '0.0.0';
diff --git a/projects/viewport/src/minimap/define.ts b/projects/viewport/src/minimap/define.ts
new file mode 100644
index 0000000000..44b1c12737
--- /dev/null
+++ b/projects/viewport/src/minimap/define.ts
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { define } from '@nvidia-elements/core/internal';
+import { ViewportMinimap } from '@nvidia-elements/viewport/minimap';
+import '@nvidia-elements/viewport/viewport/define.js';
+
+define(ViewportMinimap);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'nve-viewport-minimap': ViewportMinimap;
+ }
+}
diff --git a/projects/viewport/src/minimap/index.ts b/projects/viewport/src/minimap/index.ts
new file mode 100644
index 0000000000..c2f27af724
--- /dev/null
+++ b/projects/viewport/src/minimap/index.ts
@@ -0,0 +1,4 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export * from './minimap.js';
diff --git a/projects/viewport/src/minimap/minimap-registration.test.ts b/projects/viewport/src/minimap/minimap-registration.test.ts
new file mode 100644
index 0000000000..bb70551008
--- /dev/null
+++ b/projects/viewport/src/minimap/minimap-registration.test.ts
@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { afterEach, beforeEach, describe, expect, it } from 'vitest';
+import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
+
+describe('minimap registration', () => {
+ let fixture: HTMLElement;
+
+ beforeEach(async () => {
+ fixture = await createFixture();
+ });
+
+ afterEach(() => {
+ removeFixture(fixture);
+ });
+
+ it('renders existing markup when the minimap entrypoint loads before the viewport entrypoint', async () => {
+ fixture.innerHTML = `
+
+ content
+
+
+ `;
+ expect(customElements.get('nve-viewport')).toBeUndefined();
+
+ await import(/* @vite-ignore */ new URL('../../dist/minimap/define.js', import.meta.url).href);
+
+ expect(customElements.get('nve-viewport')).toBeDefined();
+ expect(customElements.get('nve-viewport-gridlines')).toBeUndefined();
+ const viewport = fixture.querySelector('nve-viewport');
+ const minimap = fixture.querySelector('nve-viewport-minimap');
+ if (!viewport || !minimap) {
+ throw new Error('Viewport and minimap are missing.');
+ }
+ await elementIsStable(viewport);
+ await elementIsStable(minimap);
+ await expect.poll(() => minimap.shadowRoot?.querySelector('[data-visible-region]')).toBeInstanceOf(SVGRectElement);
+ expect(minimap.shadowRoot?.querySelector('[data-item-key]')).toBeInstanceOf(SVGRectElement);
+ expect(minimap.matches(':state(pan-eligible)')).toBe(true);
+ });
+});
diff --git a/projects/core/src/viewport/viewport-minimap.css b/projects/viewport/src/minimap/minimap.css
similarity index 100%
rename from projects/core/src/viewport/viewport-minimap.css
rename to projects/viewport/src/minimap/minimap.css
diff --git a/projects/viewport/src/minimap/minimap.examples.ts b/projects/viewport/src/minimap/minimap.examples.ts
new file mode 100644
index 0000000000..d877f1965d
--- /dev/null
+++ b/projects/viewport/src/minimap/minimap.examples.ts
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/minimap/define.js';
+import '@nvidia-elements/viewport/gridlines/define.js';
+
+export default {
+ title: 'Elements/Viewport/Minimap',
+ component: 'nve-viewport-minimap'
+};
+
+/**
+ * @summary Provide custom minimap content to replace the default bounding boxes with a simplified representation of the viewport content.
+ */
+export const Default = {
+ render: () => html`
+
+
+
+
+
+
+
+
+
+ `
+};
diff --git a/projects/viewport/src/minimap/minimap.test.axe.ts b/projects/viewport/src/minimap/minimap.test.axe.ts
new file mode 100644
index 0000000000..28c2bf2dc8
--- /dev/null
+++ b/projects/viewport/src/minimap/minimap.test.axe.ts
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { html } from 'lit';
+import { afterEach, describe, expect, it } from 'vitest';
+import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
+import { runAxe } from '@internals/testing/axe';
+import { ViewportMinimap } from '@nvidia-elements/viewport/minimap';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/minimap/define.js';
+
+describe(ViewportMinimap.metadata.tag, () => {
+ let fixture: HTMLElement;
+
+ afterEach(() => removeFixture(fixture));
+
+ it('should pass axe check with interactive viewport content', async () => {
+ fixture = await createFixture(html`
+
+
+
+
+ `);
+ const viewport = fixture.querySelector('nve-viewport');
+ const minimap = fixture.querySelector('nve-viewport-minimap');
+ if (!viewport || !minimap) throw new Error('Minimap fixture is missing.');
+ await elementIsStable(viewport);
+ await elementIsStable(minimap);
+
+ const results = await runAxe(['nve-viewport']);
+ expect(results.violations.length).toBe(0);
+ });
+});
diff --git a/projects/viewport/src/minimap/minimap.test.lighthouse.ts b/projects/viewport/src/minimap/minimap.test.lighthouse.ts
new file mode 100644
index 0000000000..4f60b4ea1e
--- /dev/null
+++ b/projects/viewport/src/minimap/minimap.test.lighthouse.ts
@@ -0,0 +1,28 @@
+// 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('minimap lighthouse report', () => {
+ test('minimap should meet lighthouse benchmarks', async () => {
+ const report = await lighthouseRunner.getReport(
+ 'nve-viewport-minimap',
+ /* html */ `
+
+
+ content
+
+
+ `
+ );
+
+ expect(report.scores.performance).toBe(100);
+ expect(report.scores.accessibility).toBe(100);
+ expect(report.scores.bestPractices).toBe(100);
+ expect(report.payload.javascript.kb).toBeLessThan(25.1);
+ });
+});
diff --git a/projects/viewport/src/minimap/minimap.test.ssr.ts b/projects/viewport/src/minimap/minimap.test.ssr.ts
new file mode 100644
index 0000000000..b280c5077b
--- /dev/null
+++ b/projects/viewport/src/minimap/minimap.test.ssr.ts
@@ -0,0 +1,26 @@
+// 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 { ViewportMinimap } from '@nvidia-elements/viewport/minimap';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/minimap/define.js';
+
+describe(ViewportMinimap.metadata.tag, () => {
+ it('should render in the overlay slot on the server', async () => {
+ const result = await ssrRunner.render(html`
+
+
+ content
+
+ `);
+
+ const tag = result.match(/]*>/)?.[0];
+ expect(tag).toBeDefined();
+ expect(tag).toContain('slot="overlay"');
+ expect(tag).toContain('aria-hidden="true"');
+ expect(result.match(/shadowroot="open"/g)).toHaveLength(2);
+ });
+});
diff --git a/projects/core/src/viewport/viewport-minimap.test.ts b/projects/viewport/src/minimap/minimap.test.ts
similarity index 98%
rename from projects/core/src/viewport/viewport-minimap.test.ts
rename to projects/viewport/src/minimap/minimap.test.ts
index 1b6cad078f..b8e6c335e0 100644
--- a/projects/core/src/viewport/viewport-minimap.test.ts
+++ b/projects/viewport/src/minimap/minimap.test.ts
@@ -6,12 +6,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
import {
Viewport,
- ViewportMinimap,
type ViewportPanDetail,
type ViewportPanEndDetail,
type ViewportZoomDetail
-} from '@nvidia-elements/core/viewport';
-import '@nvidia-elements/core/viewport/define.js';
+} from '@nvidia-elements/viewport/viewport';
+import { ViewportMinimap } from '@nvidia-elements/viewport/minimap';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/minimap/define.js';
describe(ViewportMinimap.metadata.tag, () => {
let fixture: HTMLElement;
@@ -46,6 +47,10 @@ describe(ViewportMinimap.metadata.tag, () => {
vi.restoreAllMocks();
});
+ it('registers without unrelated viewport components', () => {
+ expect(customElements.get('nve-viewport-gridlines')).toBeUndefined();
+ });
+
it('defines the associated element and assigns the fixed overlay slot without a tab stop', () => {
expect(customElements.get(ViewportMinimap.metadata.tag)).toBe(ViewportMinimap);
expect(minimap.slot).toBe('overlay');
@@ -184,7 +189,9 @@ describe(ViewportMinimap.metadata.tag, () => {
await flushProjection(minimap);
expect(itemKeys(minimap)).toEqual([]);
- expect(minimap.shadowRoot?.querySelector('slot[name="preview"]')?.assignedElements()).toEqual([preview]);
+ expect(minimap.shadowRoot?.querySelector('slot[name="preview"]')?.assignedElements()).toEqual([
+ preview
+ ]);
expect(preview.querySelectorAll('circle, path')).toHaveLength(2);
expect(previewProjection(minimap).scale).toBeCloseTo(0.2);
expect(visibleRegionRect(minimap).x).toBeCloseTo(16);
diff --git a/projects/core/src/viewport/viewport.test.visual.ts b/projects/viewport/src/minimap/minimap.test.visual.ts
similarity index 51%
rename from projects/core/src/viewport/viewport.test.visual.ts
rename to projects/viewport/src/minimap/minimap.test.visual.ts
index 4bb0077145..4b539500d3 100644
--- a/projects/core/src/viewport/viewport.test.visual.ts
+++ b/projects/viewport/src/minimap/minimap.test.visual.ts
@@ -4,58 +4,28 @@
import { describe, expect, test } from 'vitest';
import { visualRunner } from '@internals/vite';
-describe('viewport visual', () => {
- test('viewport should match visual baseline', async () => {
- const report = await visualRunner.render('viewport', template());
+describe('minimap visual', () => {
+ test('minimap should match visual baseline', async () => {
+ const report = await visualRunner.render('minimap', minimapTemplate(), { waitFor: waitForMinimap });
expect(report.maxDiffPercentage).toBeLessThan(1);
});
- test('viewport should match visual baseline dark theme', async () => {
- const report = await visualRunner.render('viewport.dark', template('dark'));
- expect(report.maxDiffPercentage).toBeLessThan(1);
- });
-
- test('viewport dots should match visual baseline', async () => {
- const report = await visualRunner.render('viewport-dots', template('', 'dots'));
- expect(report.maxDiffPercentage).toBeLessThan(1);
- });
-
- test('viewport dots should match visual baseline dark theme', async () => {
- const report = await visualRunner.render('viewport-dots.dark', template('dark', 'dots'));
- expect(report.maxDiffPercentage).toBeLessThan(1);
- });
-
- test('viewport crosses should match visual baseline', async () => {
- const report = await visualRunner.render('viewport-crosses', template('', 'crosses'));
- expect(report.maxDiffPercentage).toBeLessThan(1);
- });
-
- test('viewport crosses should match visual baseline dark theme', async () => {
- const report = await visualRunner.render('viewport-crosses.dark', template('dark', 'crosses'));
- expect(report.maxDiffPercentage).toBeLessThan(1);
- });
-
- test('viewport minimap should match visual baseline', async () => {
- const report = await visualRunner.render('viewport-minimap', minimapTemplate(), { waitFor: waitForMinimap });
- expect(report.maxDiffPercentage).toBeLessThan(1);
- });
-
- test('viewport minimap should match visual baseline dark theme', async () => {
- const report = await visualRunner.render('viewport-minimap.dark', minimapTemplate('dark'), {
+ test('minimap should match visual baseline dark theme', async () => {
+ const report = await visualRunner.render('minimap.dark', minimapTemplate('dark'), {
waitFor: waitForMinimap
});
expect(report.maxDiffPercentage).toBeLessThan(1);
});
- test('viewport minimap custom should match visual baseline', async () => {
- const report = await visualRunner.render('viewport-minimap-custom', minimapTemplate('', 'custom'), {
+ test('minimap custom should match visual baseline', async () => {
+ const report = await visualRunner.render('minimap-custom', minimapTemplate('', 'custom'), {
waitFor: waitForMinimap
});
expect(report.maxDiffPercentage).toBeLessThan(1);
});
- test('viewport minimap custom should match visual baseline dark theme', async () => {
- const report = await visualRunner.render('viewport-minimap-custom.dark', minimapTemplate('dark', 'custom'), {
+ test('minimap custom should match visual baseline dark theme', async () => {
+ const report = await visualRunner.render('minimap-custom.dark', minimapTemplate('dark', 'custom'), {
waitFor: waitForMinimap
});
expect(report.maxDiffPercentage).toBeLessThan(1);
@@ -66,33 +36,6 @@ async function waitForMinimap(waitForFunction: (...args: unknown[]) => Promise document.querySelector('nve-viewport-minimap')?.hasAttribute('data-visual-ready'));
}
-function template(theme: '' | 'dark' = '', pattern: 'lines' | 'dots' | 'crosses' = 'lines') {
- return /* html */ `
-
-
-
-
- •︎•︎•︎•︎•︎
- •︎•︎•︎
-
-
- `;
-}
-
function minimapTemplate(theme: '' | 'dark' = '', preview: 'automatic' | 'custom' = 'automatic') {
const content =
preview === 'custom'
@@ -121,7 +64,9 @@ function minimapTemplate(theme: '' | 'dark' = '', preview: 'automatic' | 'custom
: '';
return /* html */ `
diff --git a/projects/core/src/viewport/viewport-minimap.ts b/projects/viewport/src/minimap/minimap.ts
similarity index 98%
rename from projects/core/src/viewport/viewport-minimap.ts
rename to projects/viewport/src/minimap/minimap.ts
index 2ae5dcfd1e..d07ebc1e96 100644
--- a/projects/core/src/viewport/viewport-minimap.ts
+++ b/projects/viewport/src/minimap/minimap.ts
@@ -15,22 +15,22 @@ import {
type WheelGestureInput,
useStyles
} from '@nvidia-elements/core/internal';
-import styles from './viewport-minimap.css?inline';
+import styles from './minimap.css?inline';
import {
createViewportMinimapProjection,
projectViewportMinimapRect,
unprojectViewportMinimapPoint,
type ViewportMinimapProjection
-} from './viewport-minimap.utils.js';
-import { anchoredTransform } from './viewport-projection.utils.js';
-import { Viewport } from './viewport.js';
+} from './minimap.utils.js';
+import { anchoredTransform } from '../viewport/viewport-projection.utils.js';
+import { Viewport } from '@nvidia-elements/viewport/viewport';
import type {
ViewportPanEndRequest,
ViewportPanUpdateProposal,
ViewportPanSession,
ViewportRect,
ViewportTransform
-} from './viewport.types.js';
+} from '@nvidia-elements/viewport/viewport';
const DRAG_THRESHOLD = 3;
@@ -61,9 +61,9 @@ interface AutomaticPreviewItem extends ViewportRect {
/**
* @element nve-viewport-minimap
* @description Provides a simplified overview and direct navigation for its parent viewport.
- * @documentation https://nvidia.github.io/elements/docs/elements/viewport/
+ * @documentation https://nvidia.github.io/elements/docs/viewport/minimap/
* @since 0.0.0
- * @entrypoint \@nvidia-elements/core/viewport
+ * @entrypoint \@nvidia-elements/viewport/minimap
* @cssprop --background - Color of the surface behind preview geometry.
* @cssprop --color - Color of automatically drawn root rectangles.
* @cssprop --indicator-color - Color of the current visible-region indicator.
diff --git a/projects/core/src/viewport/viewport-minimap.utils.test.ts b/projects/viewport/src/minimap/minimap.utils.test.ts
similarity index 74%
rename from projects/core/src/viewport/viewport-minimap.utils.test.ts
rename to projects/viewport/src/minimap/minimap.utils.test.ts
index 51406c651b..f9183c8195 100644
--- a/projects/core/src/viewport/viewport-minimap.utils.test.ts
+++ b/projects/viewport/src/minimap/minimap.utils.test.ts
@@ -2,11 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
import { describe, expect, it } from 'vitest';
-import {
- createViewportMinimapProjection,
- hasPositiveFiniteRect,
- projectViewportMinimapRect
-} from './viewport-minimap.utils.js';
+import { createViewportMinimapProjection, hasPositiveFiniteRect, projectViewportMinimapRect } from './minimap.utils.js';
describe('viewport minimap projection', () => {
it('accepts finite coordinates with positive dimensions', () => {
@@ -25,6 +21,23 @@ describe('viewport minimap projection', () => {
expect(hasPositiveFiniteRect(rect)).toBe(false);
});
+ it.each([
+ { size: { width: 0, height: 144 }, visible: { x: 0, y: 0, width: 400, height: 300 } },
+ { size: { width: 192, height: 0 }, visible: { x: 0, y: 0, width: 400, height: 300 } },
+ { size: { width: 192, height: 144 }, visible: { x: 0, y: 0, width: 0, height: 300 } },
+ { size: { width: 192, height: 144 }, visible: { x: 0, y: 0, width: 400, height: 0 } }
+ ])('does not project an empty minimap or visible area', ({ size, visible }) => {
+ expect(createViewportMinimapProjection([], visible, size)).toBeUndefined();
+ });
+
+ it('ignores invalid content bounds while projecting the visible area', () => {
+ const visible = { x: 0, y: 0, width: 400, height: 300 };
+ const size = { width: 192, height: 144 };
+ expect(createViewportMinimapProjection([{ x: Number.NaN, y: 0, width: 10, height: 10 }], visible, size)).toEqual(
+ createViewportMinimapProjection([], visible, size)
+ );
+ });
+
it('includes content bounds and the visible rectangle in its padded extent', () => {
const projection = createViewportMinimapProjection(
[{ x: 50, y: 0, width: 50, height: 100 }],
diff --git a/projects/core/src/viewport/viewport-minimap.utils.ts b/projects/viewport/src/minimap/minimap.utils.ts
similarity index 97%
rename from projects/core/src/viewport/viewport-minimap.utils.ts
rename to projects/viewport/src/minimap/minimap.utils.ts
index 10b51a07cb..2a70cfaa42 100644
--- a/projects/core/src/viewport/viewport-minimap.utils.ts
+++ b/projects/viewport/src/minimap/minimap.utils.ts
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
-import type { ViewportPoint, ViewportRect } from './viewport.types.js';
+import type { ViewportPoint, ViewportRect } from '@nvidia-elements/viewport/viewport';
const PROJECTION_PADDING = 0.1;
diff --git a/projects/viewport/src/viewport/define.ts b/projects/viewport/src/viewport/define.ts
new file mode 100644
index 0000000000..fa45555178
--- /dev/null
+++ b/projects/viewport/src/viewport/define.ts
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { define } from '@nvidia-elements/core/internal';
+import { Viewport } from '@nvidia-elements/viewport/viewport';
+
+define(Viewport);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'nve-viewport': Viewport;
+ }
+}
diff --git a/projects/core/src/viewport/index.ts b/projects/viewport/src/viewport/index.ts
similarity index 89%
rename from projects/core/src/viewport/index.ts
rename to projects/viewport/src/viewport/index.ts
index ac7b50f45e..8338b1f01c 100644
--- a/projects/core/src/viewport/index.ts
+++ b/projects/viewport/src/viewport/index.ts
@@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
export * from './viewport.js';
-export * from './viewport-gridlines.js';
-export * from './viewport-minimap.js';
export type {
ViewportAnimationOptions,
ViewportDiscretePanDetail,
diff --git a/projects/core/src/viewport/viewport-animation.controller.test.ts b/projects/viewport/src/viewport/viewport-animation.controller.test.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-animation.controller.test.ts
rename to projects/viewport/src/viewport/viewport-animation.controller.test.ts
diff --git a/projects/core/src/viewport/viewport-animation.controller.ts b/projects/viewport/src/viewport/viewport-animation.controller.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-animation.controller.ts
rename to projects/viewport/src/viewport/viewport-animation.controller.ts
diff --git a/projects/core/src/viewport/viewport-autofit.controller.test.ts b/projects/viewport/src/viewport/viewport-autofit.controller.test.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-autofit.controller.test.ts
rename to projects/viewport/src/viewport/viewport-autofit.controller.test.ts
diff --git a/projects/core/src/viewport/viewport-autofit.controller.ts b/projects/viewport/src/viewport/viewport-autofit.controller.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-autofit.controller.ts
rename to projects/viewport/src/viewport/viewport-autofit.controller.ts
diff --git a/projects/core/src/viewport/viewport-fitting.utils.test.ts b/projects/viewport/src/viewport/viewport-fitting.utils.test.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-fitting.utils.test.ts
rename to projects/viewport/src/viewport/viewport-fitting.utils.test.ts
diff --git a/projects/core/src/viewport/viewport-fitting.utils.ts b/projects/viewport/src/viewport/viewport-fitting.utils.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-fitting.utils.ts
rename to projects/viewport/src/viewport/viewport-fitting.utils.ts
diff --git a/projects/core/src/viewport/viewport-gesture-navigation.controller.test.ts b/projects/viewport/src/viewport/viewport-gesture-navigation.controller.test.ts
similarity index 99%
rename from projects/core/src/viewport/viewport-gesture-navigation.controller.test.ts
rename to projects/viewport/src/viewport/viewport-gesture-navigation.controller.test.ts
index aa29ec02b0..68d32c61e1 100644
--- a/projects/core/src/viewport/viewport-gesture-navigation.controller.test.ts
+++ b/projects/viewport/src/viewport/viewport-gesture-navigation.controller.test.ts
@@ -175,7 +175,9 @@ describe('ViewportGestureNavigationController', () => {
);
expect(sessions).toHaveLength(1);
expect(sessions[0]?.update).toHaveBeenCalledWith(expect.objectContaining({ next: { scale: 1, x: -10, y: 0 } }));
- expect(vi.mocked(sessions[0]?.update).mock.calls[0]?.[0]).not.toHaveProperty('source');
+ const session = sessions[0];
+ if (!session) throw new Error('Expected a pan session.');
+ expect(vi.mocked(session.update).mock.calls[0]?.[0]).not.toHaveProperty('source');
expect(sessions[0]?.end).toHaveBeenCalledWith(expect.objectContaining({ interrupted: false, reason: 'up' }));
expect(host.matches(':state(panning)')).toBe(false);
expect(target.dispatchEvent(pointerEvent('click', { buttons: 0, pointerId: 1 }))).toBe(false);
diff --git a/projects/core/src/viewport/viewport-gesture-navigation.controller.ts b/projects/viewport/src/viewport/viewport-gesture-navigation.controller.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-gesture-navigation.controller.ts
rename to projects/viewport/src/viewport/viewport-gesture-navigation.controller.ts
diff --git a/projects/core/src/viewport/viewport-navigation.controller.test.ts b/projects/viewport/src/viewport/viewport-navigation.controller.test.ts
similarity index 98%
rename from projects/core/src/viewport/viewport-navigation.controller.test.ts
rename to projects/viewport/src/viewport/viewport-navigation.controller.test.ts
index a2c03375f3..668477e74f 100644
--- a/projects/core/src/viewport/viewport-navigation.controller.test.ts
+++ b/projects/viewport/src/viewport/viewport-navigation.controller.test.ts
@@ -286,7 +286,9 @@ describe('ViewportNavigationController', () => {
});
it('rejects disabled input and resumes once after reconnecting', () => {
- const command = (): void => host.dispatchEvent(new CommandEvent('command', { command: '--pan-right' }));
+ const command = (): void => {
+ host.dispatchEvent(new CommandEvent('command', { command: '--pan-right' }));
+ };
command();
expect(host.requestPan).not.toHaveBeenCalled();
diff --git a/projects/core/src/viewport/viewport-navigation.controller.ts b/projects/viewport/src/viewport/viewport-navigation.controller.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-navigation.controller.ts
rename to projects/viewport/src/viewport/viewport-navigation.controller.ts
diff --git a/projects/core/src/viewport/viewport-navigation.types.ts b/projects/viewport/src/viewport/viewport-navigation.types.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-navigation.types.ts
rename to projects/viewport/src/viewport/viewport-navigation.types.ts
diff --git a/projects/core/src/viewport/viewport-projection.utils.test.ts b/projects/viewport/src/viewport/viewport-projection.utils.test.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-projection.utils.test.ts
rename to projects/viewport/src/viewport/viewport-projection.utils.test.ts
diff --git a/projects/core/src/viewport/viewport-projection.utils.ts b/projects/viewport/src/viewport/viewport-projection.utils.ts
similarity index 100%
rename from projects/core/src/viewport/viewport-projection.utils.ts
rename to projects/viewport/src/viewport/viewport-projection.utils.ts
diff --git a/projects/core/src/viewport/viewport.css b/projects/viewport/src/viewport/viewport.css
similarity index 100%
rename from projects/core/src/viewport/viewport.css
rename to projects/viewport/src/viewport/viewport.css
diff --git a/projects/core/src/viewport/viewport.examples.ts b/projects/viewport/src/viewport/viewport.examples.ts
similarity index 77%
rename from projects/core/src/viewport/viewport.examples.ts
rename to projects/viewport/src/viewport/viewport.examples.ts
index fef54ded11..d194875bda 100644
--- a/projects/core/src/viewport/viewport.examples.ts
+++ b/projects/viewport/src/viewport/viewport.examples.ts
@@ -8,7 +8,9 @@ import '@nvidia-elements/core/button-group/define.js';
import '@nvidia-elements/core/card/define.js';
import '@nvidia-elements/core/input/define.js';
import '@nvidia-elements/core/toolbar/define.js';
-import '@nvidia-elements/core/viewport/define.js';
+import '@nvidia-elements/viewport/viewport/define.js';
+import '@nvidia-elements/viewport/gridlines/define.js';
+import '@nvidia-elements/viewport/minimap/define.js';
export default {
title: 'Elements/Viewport',
@@ -42,38 +44,6 @@ export const Default = {
`
};
-/**
- * @summary Provide custom minimap content to replace the default bounding boxes with a simplified representation of the viewport content.
- */
-export const Minimap = {
- render: () => html`
-
-
-
-
-
-
-
-
-
- `
-};
/**
* @summary Place native and Elements controls inside a viewport without losing their standard interactions.
@@ -328,55 +298,7 @@ export const ProgrammaticNavigation = {
`
};
-/**
- * @summary Optionally configure viewport grid lines to use dots instead of lines.
- */
-export const Dots = {
- render: () => html`
-
-
-
-
- `
-};
-
-/**
- * @summary Use crosses as spatial reference points to help users judge positions while keeping the content between them unobstructed.
- */
-export const Crosses = {
- render: () => html`
-
-
-
-
-
- `
-};
/**
* @summary Place custom content in the background slot to provide a spatial reference without affecting content fitting.
diff --git a/projects/core/src/viewport/viewport.test.axe.ts b/projects/viewport/src/viewport/viewport.test.axe.ts
similarity index 77%
rename from projects/core/src/viewport/viewport.test.axe.ts
rename to projects/viewport/src/viewport/viewport.test.axe.ts
index 55d4a86325..0b3ee314ce 100644
--- a/projects/core/src/viewport/viewport.test.axe.ts
+++ b/projects/viewport/src/viewport/viewport.test.axe.ts
@@ -5,8 +5,8 @@ import { html } from 'lit';
import { afterEach, describe, expect, it } from 'vitest';
import { createFixture, elementIsStable, removeFixture } from '@internals/testing';
import { runAxe } from '@internals/testing/axe';
-import { Viewport } from '@nvidia-elements/core/viewport';
-import '@nvidia-elements/core/viewport/define.js';
+import { Viewport } from '@nvidia-elements/viewport/viewport';
+import '@nvidia-elements/viewport/viewport/define.js';
describe(Viewport.metadata.tag, () => {
let fixture: HTMLElement;
@@ -16,15 +16,14 @@ describe(Viewport.metadata.tag, () => {
it('should pass axe check with interactive slotted content', async () => {
fixture = await createFixture(html`
-
-
`);
- const element = fixture.querySelector(Viewport.metadata.tag);
+ const element = fixture.querySelector('nve-viewport');
+ if (!element) throw new Error('Viewport fixture is missing.');
await elementIsStable(element);
const results = await runAxe([Viewport.metadata.tag]);
diff --git a/projects/core/src/viewport/viewport.test.lighthouse.ts b/projects/viewport/src/viewport/viewport.test.lighthouse.ts
similarity index 84%
rename from projects/core/src/viewport/viewport.test.lighthouse.ts
rename to projects/viewport/src/viewport/viewport.test.lighthouse.ts
index 53cfdf94a5..fe88bbe714 100644
--- a/projects/core/src/viewport/viewport.test.lighthouse.ts
+++ b/projects/viewport/src/viewport/viewport.test.lighthouse.ts
@@ -10,12 +10,10 @@ describe('viewport lighthouse report', () => {
'nve-viewport',
/* html */ `
-
content
-
`
);
diff --git a/projects/viewport/src/viewport/viewport.test.ssr.ts b/projects/viewport/src/viewport/viewport.test.ssr.ts
new file mode 100644
index 0000000000..166aa02774
--- /dev/null
+++ b/projects/viewport/src/viewport/viewport.test.ssr.ts
@@ -0,0 +1,24 @@
+// 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 { Viewport } from '@nvidia-elements/viewport/viewport';
+import '@nvidia-elements/viewport/viewport/define.js';
+
+describe(Viewport.metadata.tag, () => {
+ it('should render content layers on the server', async () => {
+ const result = await ssrRunner.render(html`
+
+ content
+
+ `);
+
+ expect(result.includes('shadowroot="open"')).toBe(true);
+ expect(result.includes('slot name="background"')).toBe(true);
+ expect(result.includes('slot name="overlay"')).toBe(true);
+ expect(result.includes('class="plane"')).toBe(true);
+ expect(result).toContain('content
');
+ });
+});
diff --git a/projects/core/src/viewport/viewport.test.ts b/projects/viewport/src/viewport/viewport.test.ts
similarity index 99%
rename from projects/core/src/viewport/viewport.test.ts
rename to projects/viewport/src/viewport/viewport.test.ts
index 39181ba3bd..ec9da18b91 100644
--- a/projects/core/src/viewport/viewport.test.ts
+++ b/projects/viewport/src/viewport/viewport.test.ts
@@ -15,8 +15,8 @@ import {
type ViewportTransform,
type ViewportZoomDetail,
type ViewportZoomProposal
-} from '@nvidia-elements/core/viewport';
-import '@nvidia-elements/core/viewport/define.js';
+} from '@nvidia-elements/viewport/viewport';
+import '@nvidia-elements/viewport/viewport/define.js';
describe(Viewport.metadata.tag, () => {
let element: Viewport;
@@ -28,7 +28,9 @@ describe(Viewport.metadata.tag, () => {
vi.fn(() => ({ matches: false }))
);
fixture = await createFixture(html``);
- element = fixture.querySelector(Viewport.metadata.tag);
+ const viewport = fixture.querySelector('nve-viewport');
+ if (!viewport) throw new Error('Viewport fixture is missing.');
+ element = viewport;
await elementIsStable(element);
});
@@ -38,6 +40,11 @@ describe(Viewport.metadata.tag, () => {
vi.unstubAllGlobals();
});
+ it('registers without unrelated viewport components', () => {
+ expect(customElements.get('nve-viewport-gridlines')).toBeUndefined();
+ expect(customElements.get('nve-viewport-minimap')).toBeUndefined();
+ });
+
describe('transform, projection, and rendering', () => {
it('defines transform-only defaults', () => {
expect(customElements.get(Viewport.metadata.tag)).toBe(Viewport);
@@ -670,7 +677,7 @@ describe(Viewport.metadata.tag, () => {
element.dispatchEvent(pointerEvent('pointerdown', { pointerId: 1 }));
element.dispatchEvent(pointerEvent('pointermove', { clientX: 10, pointerId: 1 }));
element.addEventListener('pan', event => {
- if (event.detail.source === 'wheel') event.preventDefault();
+ if (event instanceof CustomEvent && event.detail.source === 'wheel') event.preventDefault();
});
element.dispatchEvent(wheelEvent({ deltaX: 30 }));
@@ -707,8 +714,8 @@ describe(Viewport.metadata.tag, () => {
element.behaviorZoom = true;
await elementIsStable(element);
vi.spyOn(element, 'setPointerCapture').mockImplementation(() => {});
- const zoom = vi.fn((event: CustomEvent) => {
- if (event.detail.source === 'pinch') event.preventDefault();
+ const zoom = vi.fn((event: Event) => {
+ if (event instanceof CustomEvent && event.detail.source === 'pinch') event.preventDefault();
});
const pan = vi.fn();
element.addEventListener('zoom', zoom);
diff --git a/projects/viewport/src/viewport/viewport.test.visual.ts b/projects/viewport/src/viewport/viewport.test.visual.ts
new file mode 100644
index 0000000000..c295f325ae
--- /dev/null
+++ b/projects/viewport/src/viewport/viewport.test.visual.ts
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { describe, expect, test } from 'vitest';
+import { visualRunner } from '@internals/vite';
+
+describe('viewport visual', () => {
+ test('viewport should match visual baseline', async () => {
+ const report = await visualRunner.render('viewport', template());
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+
+ test('viewport should match visual baseline dark theme', async () => {
+ const report = await visualRunner.render('viewport.dark', template('dark'));
+ expect(report.maxDiffPercentage).toBeLessThan(1);
+ });
+});
+
+function template(theme: '' | 'dark' = '') {
+ return /* html */ `
+
+
+
+ •︎•︎•︎•︎•︎
+ •︎•︎•︎
+
+
+ `;
+}
diff --git a/projects/core/src/viewport/viewport.ts b/projects/viewport/src/viewport/viewport.ts
similarity index 99%
rename from projects/core/src/viewport/viewport.ts
rename to projects/viewport/src/viewport/viewport.ts
index 1bb9de6231..96ecd3fad8 100644
--- a/projects/core/src/viewport/viewport.ts
+++ b/projects/viewport/src/viewport/viewport.ts
@@ -58,9 +58,9 @@ const BEHAVIOR_PAN_CONVERTER = {
/**
* @element nve-viewport
* @description A viewport provides a spatial surface for arbitrary content, with optional pan and zoom navigation.
- * @documentation https://nvidia.github.io/elements/docs/elements/viewport/
+ * @documentation https://nvidia.github.io/elements/docs/viewport/
* @since 0.0.0
- * @entrypoint \@nvidia-elements/core/viewport
+ * @entrypoint \@nvidia-elements/viewport/viewport
* @slot - Content positioned in the viewport's infinite content space.
* @slot background - Custom content rendered behind viewport content and excluded from content fitting
* @slot overlay - Interactive content fixed above the transformed plane and excluded from content fitting
diff --git a/projects/core/src/viewport/viewport.types.ts b/projects/viewport/src/viewport/viewport.types.ts
similarity index 100%
rename from projects/core/src/viewport/viewport.types.ts
rename to projects/viewport/src/viewport/viewport.types.ts
diff --git a/projects/viewport/tsconfig.json b/projects/viewport/tsconfig.json
new file mode 100644
index 0000000000..8c341efb7a
--- /dev/null
+++ b/projects/viewport/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "noUncheckedSideEffectImports": true,
+ "skipLibCheck": true,
+ "module": "es2022",
+ "target": "es2022",
+ "lib": ["es2022", "dom", "dom.iterable"],
+ "typeRoots": ["./node_modules/@types"],
+ "declaration": true,
+ "emitDeclarationOnly": true,
+ "outDir": "dist/",
+ "rootDir": "./",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ "isolatedModules": true,
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "paths": {
+ "@nvidia-elements/viewport": ["./src/index.js"],
+ "@nvidia-elements/viewport/*": ["./src/*"],
+ "@nvidia-elements/viewport/*.js": ["./src/*.js"]
+ }
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/projects/viewport/tsconfig.lib.json b/projects/viewport/tsconfig.lib.json
new file mode 100644
index 0000000000..af21f6f61c
--- /dev/null
+++ b/projects/viewport/tsconfig.lib.json
@@ -0,0 +1,15 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "types": []
+ },
+ "exclude": [
+ "**/*.test.ts",
+ "**/*.test.axe.ts",
+ "**/*.test.ssr.ts",
+ "**/*.test.visual.ts",
+ "**/*.test.lighthouse.ts",
+ "**/*.examples.ts"
+ ]
+}
diff --git a/projects/viewport/vite.config.ts b/projects/viewport/vite.config.ts
new file mode 100644
index 0000000000..eeec18347b
--- /dev/null
+++ b/projects/viewport/vite.config.ts
@@ -0,0 +1,13 @@
+import { resolve } from 'path';
+import { UserConfig, defineConfig, mergeConfig } from 'vite';
+import { libraryBuildConfig } from '@internals/vite';
+
+export default defineConfig(() => {
+ const config: UserConfig = {
+ resolve: {
+ alias: { '@nvidia-elements/viewport': resolve(import.meta.dirname, './src') }
+ }
+ };
+
+ return mergeConfig(libraryBuildConfig, config);
+});
diff --git a/projects/viewport/vitest.axe.ts b/projects/viewport/vitest.axe.ts
new file mode 100644
index 0000000000..01d7a43bce
--- /dev/null
+++ b/projects/viewport/vitest.axe.ts
@@ -0,0 +1,20 @@
+import { resolve } from 'path';
+import { mergeConfig } from 'vitest/config';
+import { libraryAxeTestConfig } from '@internals/vite/configs/axe.js';
+
+export default mergeConfig(libraryAxeTestConfig, {
+ root: import.meta.dirname,
+ resolve: {
+ alias: { '@nvidia-elements/viewport': resolve(import.meta.dirname, './src') }
+ },
+ test: {
+ include: ['./src/**/*.test.axe.ts'],
+ outputFile: {
+ junit: './coverage/axe/junit.xml'
+ },
+ coverage: {
+ provider: 'istanbul',
+ reportsDirectory: './coverage/axe'
+ }
+ }
+});
diff --git a/projects/viewport/vitest.config.ts b/projects/viewport/vitest.config.ts
new file mode 100644
index 0000000000..29df52f6ab
--- /dev/null
+++ b/projects/viewport/vitest.config.ts
@@ -0,0 +1,23 @@
+import { resolve } from 'path';
+import { mergeConfig } from 'vitest/config';
+import { libraryTestConfig } from '@internals/vite/configs/test.js';
+
+const config = mergeConfig(libraryTestConfig, {
+ root: import.meta.dirname,
+ resolve: {
+ alias: { '@nvidia-elements/viewport': resolve(import.meta.dirname, './src') }
+ },
+ test: {
+ include: ['./src/**/*.test.ts'],
+ coverage: {
+ thresholds: {
+ lines: 90,
+ branches: 90,
+ functions: 90,
+ statements: 90
+ }
+ }
+ }
+});
+
+export default config;
diff --git a/projects/viewport/vitest.lighthouse.html b/projects/viewport/vitest.lighthouse.html
new file mode 100644
index 0000000000..ca07455545
--- /dev/null
+++ b/projects/viewport/vitest.lighthouse.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ Lighthouse
+
+
+
+ Lighthouse Test
+
+
diff --git a/projects/viewport/vitest.lighthouse.ts b/projects/viewport/vitest.lighthouse.ts
new file mode 100644
index 0000000000..58bf11a01d
--- /dev/null
+++ b/projects/viewport/vitest.lighthouse.ts
@@ -0,0 +1,11 @@
+import { mergeConfig } from 'vitest/config';
+import { libraryLighthouseTestConfig } from '@internals/vite/configs/lighthouse.js';
+
+export default mergeConfig(libraryLighthouseTestConfig, {
+ test: {
+ include: ['src/**/*.test.lighthouse.ts'],
+ outputFile: {
+ junit: './coverage/lighthouse/junit.xml'
+ }
+ }
+});
diff --git a/projects/viewport/vitest.ssr.ts b/projects/viewport/vitest.ssr.ts
new file mode 100644
index 0000000000..3de60fdde3
--- /dev/null
+++ b/projects/viewport/vitest.ssr.ts
@@ -0,0 +1,15 @@
+import { resolve } from 'path';
+import { mergeConfig } from 'vitest/config';
+import { libraryLitSSRTestConfig } from '@internals/vite/configs/ssr.js';
+
+export default mergeConfig(libraryLitSSRTestConfig, {
+ resolve: {
+ alias: { '@nvidia-elements/viewport': resolve(import.meta.dirname, './dist') }
+ },
+ test: {
+ include: ['./src/**/*.test.ssr.ts'],
+ outputFile: {
+ junit: './coverage/ssr/junit.xml'
+ }
+ }
+});
diff --git a/projects/viewport/vitest.visual.html b/projects/viewport/vitest.visual.html
new file mode 100644
index 0000000000..9f772650a6
--- /dev/null
+++ b/projects/viewport/vitest.visual.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+ Visual
+
+
+
+
diff --git a/projects/viewport/vitest.visual.ts b/projects/viewport/vitest.visual.ts
new file mode 100644
index 0000000000..58fed0d07f
--- /dev/null
+++ b/projects/viewport/vitest.visual.ts
@@ -0,0 +1,12 @@
+import { mergeConfig } from 'vitest/config';
+import { libraryVisualTestConfig } from '@internals/vite/configs/visual.js';
+
+export default mergeConfig(libraryVisualTestConfig, {
+ root: import.meta.dirname,
+ test: {
+ include: ['src/**/*.test.visual.ts'],
+ outputFile: {
+ junit: './coverage/visual/junit.xml'
+ }
+ }
+});