diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 383225572e..facf5d3890 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1750,23 +1750,23 @@ importers: projects/starters/angular: dependencies: '@angular/common': - specifier: 22.1.7 - version: 22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: 22.2.0 + version: 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2) '@angular/compiler': - specifier: 22.1.7 - version: 22.1.7 + specifier: 22.2.0 + version: 22.2.0 '@angular/core': - specifier: 22.1.7 - version: 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) + specifier: 22.2.0 + version: 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2) '@angular/forms': - specifier: 22.1.7 - version: 22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)))(rxjs@7.8.2) + specifier: 22.2.0 + version: 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)))(rxjs@7.8.2) '@angular/platform-browser': - specifier: 22.1.7 - version: 22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)) + specifier: 22.2.0 + version: 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)) '@angular/router': - specifier: 22.1.7 - version: 22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)))(rxjs@7.8.2) + specifier: 22.2.0 + version: 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)))(rxjs@7.8.2) '@nvidia-elements/code': specifier: workspace:* version: link:../../code @@ -1797,13 +1797,13 @@ importers: devDependencies: '@angular/build': specifier: 22.1.7 - version: 22.1.7(@angular/compiler-cli@22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3))(@angular/compiler@22.1.7)(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)))(@types/node@26.5.0)(chokidar@5.0.0)(jiti@2.7.0)(postcss@8.5.26)(rollup@4.60.4)(sugarss@5.0.1(postcss@8.5.26))(supports-color@10.2.2)(terser@5.48.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@5.0.0)(yaml@2.9.0) + version: 22.1.7(@angular/compiler-cli@22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3))(@angular/compiler@22.2.0)(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)))(@types/node@26.5.0)(chokidar@5.0.0)(jiti@2.7.0)(postcss@8.5.26)(rollup@4.60.4)(sugarss@5.0.1(postcss@8.5.26))(supports-color@10.2.2)(terser@5.48.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@5.0.0)(yaml@2.9.0) '@angular/cli': specifier: 22.1.7 version: 22.1.7(@types/node@26.5.0)(chokidar@5.0.0)(supports-color@10.2.2) '@angular/compiler-cli': - specifier: 22.1.7 - version: 22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3) + specifier: 22.2.0 + version: 22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3) '@nvidia-elements/lint': specifier: workspace:* version: link:../../lint @@ -2807,33 +2807,33 @@ packages: engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular/common@22.1.7': - resolution: {integrity: sha512-SLW4AdH2Y8G9SM/iFRNpCQtkmvq55RcIQWsADT9ypZ7FxxcnJRsr4Q/WnbUBbuCslsCkrgzU1i9WDiW0m7gcnw==} + '@angular/common@22.2.0': + resolution: {integrity: sha512-BUKkncKwnaEcF/qatBOpzSIx2NthFZLYlTcE7iHwwsw+XQrwB9hU8Ca/l5HqaTJiMg0D8MjI/SWHnD4LFtNN+w==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/core': 22.1.7 + '@angular/core': 22.2.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/compiler-cli@22.1.7': - resolution: {integrity: sha512-VGg8Kyt0YymQfnUUFk79vFaM+x5xmIMHy+BZh3P2Lh/uxqCXTWhJJH8kOHKtIGGH5/wKhBEmSy2CezdcIuM9xg==} + '@angular/compiler-cli@22.2.0': + resolution: {integrity: sha512-xDOvPlNv+RT7sE01wjuGFRMc5SDxWs3RZpGfO5NoI3a0jxM87UW49HmmgwQ6PaU0fya9mttNkHufhfOAbh0ZKQ==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} hasBin: true peerDependencies: - '@angular/compiler': 22.1.7 + '@angular/compiler': 22.2.0 typescript: '>=6.0 <6.1' peerDependenciesMeta: typescript: optional: true - '@angular/compiler@22.1.7': - resolution: {integrity: sha512-qhoHAByh805nT4Xy4Wd2kE5FvkTc7wcg8Qps+EH6+z4KRfwGiL5cwdgooGWjTAldFCeqTgJ0BO/0dB6GHZjdkQ==} + '@angular/compiler@22.2.0': + resolution: {integrity: sha512-bnqql/HfhnhDXoS7isNaXJAsbKYszcH+nFVWA6YhiAVZhqo3f+XoQb30qeqNtEOKuuC7SZfaAJ3jBkAvyC5CKg==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} - '@angular/core@22.1.7': - resolution: {integrity: sha512-T9bt1PIgoN7n+TxGHTUgu0EsomlGMQ84YMXbMB+N6HhD8C1qZxn0uCbnAtCUjcecg1am8ioKxNTO0eII9ouioQ==} + '@angular/core@22.2.0': + resolution: {integrity: sha512-VhAgJ4GCdGgAy9+zADpSyEM9gt4pKg6+cqNeK3RBNeF3M3B8y7AWfD/nYRdM3Xzi152Pu+ipgFFNHpupCLTsvQ==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/compiler': 22.1.7 + '@angular/compiler': 22.2.0 rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.15.0 || ~0.16.0 peerDependenciesMeta: @@ -2842,33 +2842,33 @@ packages: zone.js: optional: true - '@angular/forms@22.1.7': - resolution: {integrity: sha512-oc0DT39C3ZboJpDx8xKUCltgn56LHi0kbv+ThNK/dCSZLgjF2nd+muMMRYF+amdljQp2q7+hm2ORmhON3CNFog==} + '@angular/forms@22.2.0': + resolution: {integrity: sha512-Nm8o1uBk31EuQGzsK5EunNBVFzTN9MuTYLSrmg1SCPTcava4qK3AzSpl1+Put+cZTTnGtEBIy45AFaxHR2oryg==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/common': 22.1.7 - '@angular/core': 22.1.7 - '@angular/platform-browser': 22.1.7 + '@angular/common': 22.2.0 + '@angular/core': 22.2.0 + '@angular/platform-browser': 22.2.0 rxjs: ^6.5.3 || ^7.4.0 - '@angular/platform-browser@22.1.7': - resolution: {integrity: sha512-4dT3qGw3ZHVQUcHKALzMx9MTo+pxAMzHIPxc8HUESklqXfq6GRaIIfFMvE0RsquI9iqu6admD6lOAEzLWS8DxA==} + '@angular/platform-browser@22.2.0': + resolution: {integrity: sha512-XMUOyn8WARk9NdFGO3uRgdC3hPrp+2xsSYNeSkaEZ4jI3Pc23f22e4GI8Vayl/UOmShHVn/i+NWfTWh3SEbbSw==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/animations': 22.1.7 - '@angular/common': 22.1.7 - '@angular/core': 22.1.7 + '@angular/animations': 22.2.0 + '@angular/common': 22.2.0 + '@angular/core': 22.2.0 peerDependenciesMeta: '@angular/animations': optional: true - '@angular/router@22.1.7': - resolution: {integrity: sha512-CxYa2Ym5L0WmqToHU2I0Z4VEpocdryLlvx5svLfo9M3uOaP3/9icB/gjS/vXPbbQxV8/GZ59E/7/LF14anIoRg==} + '@angular/router@22.2.0': + resolution: {integrity: sha512-+pxHMwWWKg+ejS1vKa+tS91CDusCIK1MOSy6fVLc89McMOY/C8tsLrh1uJaMjklejsz+3XUhJgrLOvl3ISNz6w==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} peerDependencies: - '@angular/common': 22.1.7 - '@angular/core': 22.1.7 - '@angular/platform-browser': 22.1.7 + '@angular/common': 22.2.0 + '@angular/core': 22.2.0 + '@angular/platform-browser': 22.2.0 rxjs: ^6.5.3 || ^7.4.0 '@anthropic-ai/sdk@0.124.0': @@ -15030,12 +15030,12 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular/build@22.1.7(@angular/compiler-cli@22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3))(@angular/compiler@22.1.7)(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)))(@types/node@26.5.0)(chokidar@5.0.0)(jiti@2.7.0)(postcss@8.5.26)(rollup@4.60.4)(sugarss@5.0.1(postcss@8.5.26))(supports-color@10.2.2)(terser@5.48.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@5.0.0)(yaml@2.9.0)': + '@angular/build@22.1.7(@angular/compiler-cli@22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3))(@angular/compiler@22.2.0)(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)))(@types/node@26.5.0)(chokidar@5.0.0)(jiti@2.7.0)(postcss@8.5.26)(rollup@4.60.4)(sugarss@5.0.1(postcss@8.5.26))(supports-color@10.2.2)(terser@5.48.0)(tslib@2.8.1)(typescript@6.0.3)(vitest@5.0.0)(yaml@2.9.0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2201.7(chokidar@5.0.0) - '@angular/compiler': 22.1.7 - '@angular/compiler-cli': 22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3) + '@angular/compiler': 22.2.0 + '@angular/compiler-cli': 22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3) '@babel/core': 8.0.1 '@babel/helper-annotate-as-pure': 8.0.0 '@babel/helper-split-export-declaration': 7.24.7 @@ -15063,8 +15063,8 @@ snapshots: vite: 8.1.5(@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) watchpack: 2.5.2 optionalDependencies: - '@angular/core': 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) - '@angular/platform-browser': 22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)) + '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2) + '@angular/platform-browser': 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)) lmdb: 3.5.6 postcss: 8.5.26 rollup: 4.60.4 @@ -15105,15 +15105,15 @@ snapshots: - chokidar - supports-color - '@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2)': + '@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@angular/core': 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) + '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3)': + '@angular/compiler-cli@22.2.0(@angular/compiler@22.2.0)(typescript@6.0.3)': dependencies: - '@angular/compiler': 22.1.7 + '@angular/compiler': 22.2.0 '@babel/core': 8.0.1 '@jridgewell/sourcemap-codec': 1.5.5 chokidar: 5.0.0 @@ -15125,38 +15125,38 @@ snapshots: optionalDependencies: typescript: 6.0.3 - '@angular/compiler@22.1.7': + '@angular/compiler@22.2.0': dependencies: tslib: 2.8.1 - '@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)': + '@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)': dependencies: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@angular/compiler': 22.1.7 + '@angular/compiler': 22.2.0 - '@angular/forms@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)))(rxjs@7.8.2)': + '@angular/forms@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)))(rxjs@7.8.2)': dependencies: - '@angular/common': 22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2) - '@angular/core': 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) - '@angular/platform-browser': 22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)) + '@angular/common': 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2) + '@angular/platform-browser': 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)) '@standard-schema/spec': 1.1.0 rxjs: 7.8.2 tslib: 2.8.1 zod: 4.5.4 - '@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))': + '@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))': dependencies: - '@angular/common': 22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2) - '@angular/core': 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) + '@angular/common': 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2) tslib: 2.8.1 - '@angular/router@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)))(rxjs@7.8.2)': + '@angular/router@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(@angular/platform-browser@22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)))(rxjs@7.8.2)': dependencies: - '@angular/common': 22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2) - '@angular/core': 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) - '@angular/platform-browser': 22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)) + '@angular/common': 22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2) + '@angular/platform-browser': 22.2.0(@angular/common@22.2.0(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.2.0(@angular/compiler@22.2.0)(rxjs@7.8.2)) rxjs: 7.8.2 tslib: 2.8.1 @@ -28017,7 +28017,7 @@ snapshots: 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.24))(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 + picomatch: 4.0.7 postcss: 8.5.26 rolldown: 1.2.6 tinyglobby: 0.2.17 @@ -28034,7 +28034,7 @@ snapshots: 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): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 + picomatch: 4.0.7 postcss: 8.5.26 rolldown: 1.2.6 tinyglobby: 0.2.17 @@ -28051,7 +28051,7 @@ snapshots: 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): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 + picomatch: 4.0.7 postcss: 8.5.26 rolldown: 1.2.6 tinyglobby: 0.2.17 @@ -28068,7 +28068,7 @@ snapshots: vite@8.2.2(@types/node@26.5.0)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.1(postcss@8.5.26))(terser@5.48.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 + picomatch: 4.0.7 postcss: 8.5.26 rolldown: 1.2.6 tinyglobby: 0.2.17 diff --git a/projects/cli/vite.config.ts b/projects/cli/vite.config.ts index 675ebf2889..5a82c4bc79 100644 --- a/projects/cli/vite.config.ts +++ b/projects/cli/vite.config.ts @@ -14,9 +14,7 @@ const MCP_UI_ENTRYPOINTS = ['api-icons-list.html', 'api-tokens-list.html', 'exam const MCP_UI_INLINE_MODULE_ID = 'virtual:mcp-ui-inline'; const PACKAGE_JSON_PATH = resolve(import.meta.dirname, 'package.json'); -export default defineConfig(async ({ command }) => { - if (command === 'build') await buildMcpUiResources(); - +export default defineConfig(() => { const libConfig = libraryNodeBuildConfig; const rollupOptions = libConfig.build?.rolldownOptions; if (rollupOptions?.external) rollupOptions.external = NODE_BUILT_IN_MODULES; @@ -53,6 +51,9 @@ function loadMcpUiResourcesPlugin(): Plugin { return { name: 'load-mcp-ui-resources', enforce: 'pre', + async buildStart() { + await buildMcpUiResources(); + }, load(id) { const htmlPath = id.match(/^(.+\.html)\?raw(?:$|&)/)?.[1]; if (!htmlPath || dirname(htmlPath) !== MCP_UI_SOURCE_DIR) return null; diff --git a/projects/core/.visual/menu-group.dark.png b/projects/core/.visual/menu-group.dark.png new file mode 100644 index 0000000000..f9dc5c6255 --- /dev/null +++ b/projects/core/.visual/menu-group.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:871f20761705cf6dd7501e02926b0d5d357b6a842c30b385d9760a89113cefa2 +size 4119 diff --git a/projects/core/.visual/menu-group.png b/projects/core/.visual/menu-group.png new file mode 100644 index 0000000000..232261588a --- /dev/null +++ b/projects/core/.visual/menu-group.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca4a779f4227ec2ea614e3f6db240de01734a959c15775ecb084df38ef87855d +size 3992 diff --git a/projects/core/src/combobox/combobox.test.lighthouse.ts b/projects/core/src/combobox/combobox.test.lighthouse.ts index 7591c8c220..7bad0a34bc 100644 --- a/projects/core/src/combobox/combobox.test.lighthouse.ts +++ b/projects/core/src/combobox/combobox.test.lighthouse.ts @@ -25,7 +25,7 @@ describe('combobox lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(36.7); + expect(report.payload.javascript.kb).toBeLessThan(36.9); }); test('combobox multi select with large dataset should meet lighthouse benchmarks', async () => { @@ -42,6 +42,6 @@ describe('combobox lighthouse report', () => { expect(report.scores.performance).toBeGreaterThanOrEqual(96); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(36.1); + expect(report.payload.javascript.kb).toBeLessThan(36.2); }); }); diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts index 6053cc25ae..fcb5391055 100644 --- a/projects/core/src/index.test.lighthouse.ts +++ b/projects/core/src/index.test.lighthouse.ts @@ -18,7 +18,7 @@ describe('lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(122.8); + expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(123.5); // if sudden drop in size, check vite bundle config and bundle demo to ensure side effects are properly preserved expect(report.payload.javascript.requests['index.js'].kb).toBeGreaterThan(100); @@ -108,7 +108,7 @@ describe('lighthouse report', () => { expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); expect(report.payload.javascript.requests[Object.keys(report.payload.javascript.requests)[0]].kb).toBeLessThan( - 103.0 + 104.0 ); }); }); diff --git a/projects/core/src/internal/types/index.ts b/projects/core/src/internal/types/index.ts index 0100216633..51691e0ed9 100644 --- a/projects/core/src/internal/types/index.ts +++ b/projects/core/src/internal/types/index.ts @@ -57,9 +57,14 @@ export type Prominence = 'emphasis' | 'muted'; * - `inline` Element container reduces to fit within inline content such as a block of text. * - `inset` Element container optimizes for embedding or inset placement inside another containing element. * - `full` Element container optimizes for filling its container bounds. - * - `condensed` Element container optimizes for small, summarized or contained spaces. */ -export type Container = 'inline' | 'flat' | 'inset' | 'full' | 'condensed'; +export type Container = 'inline' | 'flat' | 'inset' | 'full'; + +/** Controls the visual density of an element to match its context and available space. + * - `compact` - Reduces visual weight and spacing for dense layouts or secondary elements with less visual prominence. + * - `default` - Standard size that works well in most contexts and provides balanced visibility. + */ +export type Density = 'compact' | 'default'; /** Communicates the intent and semantic meaning of an element to help users understand the outcome of their actions. * - `accent` - Highlights important actions or draws attention to primary interactive elements. diff --git a/projects/core/src/internal/utils/audit.test.ts b/projects/core/src/internal/utils/audit.test.ts index 1185d26278..7e6e7d4fd6 100644 --- a/projects/core/src/internal/utils/audit.test.ts +++ b/projects/core/src/internal/utils/audit.test.ts @@ -16,7 +16,7 @@ class AuditTestElement extends LitElement { }; render() { - return html``; + return html``; } } @@ -63,6 +63,7 @@ describe('audit', () => {

+
`); element = fixture.querySelector('audit-test-element') as AuditTestElement; diff --git a/projects/core/src/internal/utils/audit.ts b/projects/core/src/internal/utils/audit.ts index d81c77abe9..78a8903144 100644 --- a/projects/core/src/internal/utils/audit.ts +++ b/projects/core/src/internal/utils/audit.ts @@ -5,9 +5,9 @@ export function auditSlots( host: HTMLElement & { constructor: { metadata?: { children?: string[]; disallowedChildren?: string[] } } } ): [Element[], string[]] { const validElements = ['template', ...(host.constructor.metadata?.children ?? [])].filter(c => typeof c === 'string'); - const invalidElements = Array.from(host.shadowRoot!.querySelectorAll('slot')).flatMap(slot => - slot.assignedElements().filter(e => !validElements.map(i => i).includes(e.localName)) - ); + const invalidElements = Array.from(host.shadowRoot!.querySelectorAll('slot')) + .filter(slot => slot.name === '') + .flatMap(slot => slot.assignedElements().filter(e => !validElements.map(i => i).includes(e.localName))); return [invalidElements, validElements]; } diff --git a/projects/core/src/menu/define.ts b/projects/core/src/menu/define.ts index 13fda8220e..1c7c016349 100644 --- a/projects/core/src/menu/define.ts +++ b/projects/core/src/menu/define.ts @@ -2,14 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 import { define } from '@nvidia-elements/core/internal'; -import { Menu, MenuItem } from '@nvidia-elements/core/menu'; +import { Menu, MenuGroup, MenuItem } from '@nvidia-elements/core/menu'; define(Menu); +define(MenuGroup); define(MenuItem); declare global { interface HTMLElementTagNameMap { 'nve-menu': Menu; + 'nve-menu-group': MenuGroup; 'nve-menu-item': MenuItem; } } diff --git a/projects/core/src/menu/index.ts b/projects/core/src/menu/index.ts index 4abd7edc63..3b9518b3e7 100644 --- a/projects/core/src/menu/index.ts +++ b/projects/core/src/menu/index.ts @@ -2,4 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 export * from './menu.js'; +export * from './menu-group.js'; export * from './menu-item.js'; diff --git a/projects/core/src/menu/menu-group.css b/projects/core/src/menu/menu-group.css new file mode 100644 index 0000000000..9d63fda73f --- /dev/null +++ b/projects/core/src/menu/menu-group.css @@ -0,0 +1,107 @@ +/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */ +/* SPDX-License-Identifier: Apache-2.0 */ + +:host { + --background: transparent; + --font-size: var(--nve-ref-font-size-100); + --padding: var(--nve-ref-size-200); + --gap: var(--nve-ref-space-sm); + --border-radius: var(--nve-ref-border-radius-xs); + --color: var(--nve-sys-interaction-color); + --width: 100%; + --min-height: var(--nve-ref-size-800); + --nve-sys-interaction-state-base: var(--background, var(--nve-sys-layer-canvas-background)); + display: block; + width: var(--width); + contain: layout; +} + +[internal-host] { + display: flex; + flex-direction: column; + width: 100%; + gap: var(--nve-ref-space-xxs); +} + +button { + appearance: none; + padding: var(--padding); + color: var(--color); + background: var(--background); + font-size: var(--font-size); + border-radius: var(--border-radius); + min-height: var(--min-height); + gap: var(--gap); + font-weight: var(--nve-ref-font-weight-medium); + border: 0; + width: 100%; + margin: 0; + font-family: inherit; + line-height: normal; + text-align: start; + cursor: pointer; + outline-offset: calc(-1 * var(--nve-ref-size-50)); + position: relative; + display: flex; + align-items: center; + background-image: linear-gradient( + color-mix( + in oklab, + var(--nve-sys-interaction-state-base) 100%, + var(--nve-sys-interaction-state-mix) var(--nve-sys-interaction-state-ratio) + ) + 0 0 + ) !important; + + &:hover { + --nve-sys-interaction-state-ratio: var(--nve-sys-interaction-state-ratio-hover); + } +} + +button:not([disabled]):focus-visible { + outline: Highlight solid 2px !important; + outline: 5px auto -webkit-focus-ring-color !important; +} + +slot:not([name]) { + display: inline-flex; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +[part='caret'] { + flex: none; + --color: currentColor; +} + +slot[name='menu'] { + width: 100%; + display: block; +} + +::slotted(nve-menu) { + --min-width: 0; + --width: 100%; + --padding: 0 0 0 var(--nve-ref-size-200); +} + +::slotted(nve-icon) { + --color: currentColor; +} + +:host([disabled]) { + --color: var(--nve-sys-interaction-disabled-color); +} + +:host([disabled]) button { + --nve-sys-interaction-state-ratio: 0; + cursor: not-allowed; + opacity: var(--nve-ref-opacity-500); +} + +:host(:not([disabled]):hover) { + --color: var(--nve-sys-interaction-selected-color); +} diff --git a/projects/core/src/menu/menu-group.test.axe.ts b/projects/core/src/menu/menu-group.test.axe.ts new file mode 100644 index 0000000000..4956381b79 --- /dev/null +++ b/projects/core/src/menu/menu-group.test.axe.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createFixture, elementIsStable, removeFixture } from '@internals/testing'; +import { runAxe } from '@internals/testing/axe'; +import { MenuGroup } from '@nvidia-elements/core/menu'; +import '@nvidia-elements/core/dot/define.js'; +import '@nvidia-elements/core/icon/define.js'; +import '@nvidia-elements/core/menu/define.js'; + +describe(MenuGroup.metadata.tag, () => { + let fixture: HTMLElement; + + beforeEach(async () => { + fixture = await createFixture(html` + + Collapsed + Item + + + + Expanded + 2 + + Current item + Other item + + + + Disabled + Visible item + + `); + + await Promise.all( + Array.from(fixture.querySelectorAll(MenuGroup.metadata.tag)).map(element => elementIsStable(element)) + ); + }); + + afterEach(() => { + removeFixture(fixture); + }); + + it('should pass axe check', async () => { + const results = await runAxe([MenuGroup.metadata.tag]); + expect(results.violations.length).toBe(0); + }); +}); diff --git a/projects/core/src/menu/menu-group.test.lighthouse.ts b/projects/core/src/menu/menu-group.test.lighthouse.ts new file mode 100644 index 0000000000..5f2f3b2b6d --- /dev/null +++ b/projects/core/src/menu/menu-group.test.lighthouse.ts @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, test } from 'vitest'; +import { lighthouseRunner } from '@internals/vite'; + +describe('menu-group lighthouse report', () => { + test('menu-group should meet lighthouse benchmarks', async () => { + const report = await lighthouseRunner.getReport( + 'nve-menu-group', + /* html */ ` + + Resources + 2 + + Documentation + Examples + + + + ` + ); + + expect(report.scores.performance).toBe(100); + expect(report.scores.accessibility).toBe(100); + expect(report.scores.bestPractices).toBe(100); + expect(report.payload.javascript.kb).toBeLessThan(24); + }); +}); diff --git a/projects/core/src/menu/menu-group.test.ssr.ts b/projects/core/src/menu/menu-group.test.ssr.ts new file mode 100644 index 0000000000..14e5c26396 --- /dev/null +++ b/projects/core/src/menu/menu-group.test.ssr.ts @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import { describe, expect, it } from 'vitest'; +import { ssrRunner } from '@internals/vite'; +import { MenuGroup } from '@nvidia-elements/core/menu'; +import '@nvidia-elements/core/menu/define.js'; + +describe(MenuGroup.metadata.tag, () => { + it('should pass baseline ssr check', async () => { + const result = await ssrRunner.render(html` + + Resources + + Documentation + + + `); + + expect(result.includes('shadowroot="open"')).toBe(true); + expect(result.includes('nve-menu-group')).toBe(true); + expect(result.includes('nve-menu')).toBe(true); + expect(result.includes('slot="menu"')).toBe(true); + expect(result.includes('aria-controls="content"')).toBe(true); + }); +}); diff --git a/projects/core/src/menu/menu-group.test.ts b/projects/core/src/menu/menu-group.test.ts new file mode 100644 index 0000000000..1f36abc5f2 --- /dev/null +++ b/projects/core/src/menu/menu-group.test.ts @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createFixture, elementIsStable, emulateClick, removeFixture, untilEvent } from '@internals/testing'; +import { Menu, MenuGroup } from '@nvidia-elements/core/menu'; +import '@nvidia-elements/core/menu/define.js'; + +describe(MenuGroup.metadata.tag, () => { + let fixture: HTMLElement; + let element: MenuGroup; + let menu: Menu; + let trigger: HTMLButtonElement; + let content: HTMLElement; + + beforeEach(async () => { + fixture = await createFixture(html` + + Labs Projects + 4 + + Markdown + Code + + + `); + element = fixture.querySelector(MenuGroup.metadata.tag); + menu = element.querySelector(Menu.metadata.tag); + trigger = element.shadowRoot.querySelector('#trigger'); + content = element.shadowRoot.querySelector('#content'); + await Promise.all([elementIsStable(element), elementIsStable(menu)]); + }); + + afterEach(() => { + removeFixture(fixture); + }); + + it('should define the element and initialize disclosure defaults', () => { + expect(customElements.get(MenuGroup.metadata.tag)).toBeDefined(); + expect(element.expanded).toBe(false); + expect(element.disabled).toBe(false); + expect(element.behaviorExpand).toBe(false); + expect(element._internals.role).toBe('group'); + expect(content.hidden).toBe(true); + }); + + it('should automatically project its direct menu into the content slot', () => { + expect(menu.slot).toBe('menu'); + expect(element.shadowRoot.querySelector('slot[name="menu"]').assignedElements()).toEqual([menu]); + }); + + it('should remove an automatically assigned slot when the menu leaves the group', () => { + fixture.append(menu); + expect(menu.slot).toBe(''); + + element.append(menu); + expect(menu.slot).toBe('menu'); + }); + + it('should preserve an explicitly assigned slot', () => { + fixture.append(menu); + menu.slot = 'custom'; + element.append(menu); + expect(menu.slot).toBe('custom'); + + fixture.append(menu); + expect(menu.slot).toBe('custom'); + }); + + it('should expose the disclosure state and relationship on the trigger', async () => { + expect(trigger.type).toBe('button'); + expect(trigger.getAttribute('aria-expanded')).toBe('false'); + expect(trigger.getAttribute('aria-controls')).toBe('content'); + + element.expanded = true; + await elementIsStable(element); + expect(trigger.getAttribute('aria-expanded')).toBe('true'); + expect(content.hidden).toBe(false); + }); + + it('should emit intent without changing state by default', async () => { + const event = untilEvent(element, 'open'); + await emulateClick(trigger); + const openEvent = await event; + + expect(element.expanded).toBe(false); + expect(openEvent.bubbles).toBe(true); + expect(openEvent.composed).toBe(true); + }); + + it('should automatically toggle state when behavior-expand is enabled', async () => { + element.behaviorExpand = true; + await elementIsStable(element); + + const open = untilEvent(element, 'open'); + await emulateClick(trigger); + await open; + await elementIsStable(element); + expect(element.expanded).toBe(true); + + const close = untilEvent(element, 'close'); + await emulateClick(trigger); + await close; + await elementIsStable(element); + expect(element.expanded).toBe(false); + }); + + it('should use a native button for keyboard activation and focus behavior', () => { + expect(trigger).toBeInstanceOf(HTMLButtonElement); + expect(trigger.tabIndex).toBe(0); + }); + + it('should not respond to header activation when disabled', async () => { + const open = vi.fn(); + element.disabled = true; + element.behaviorExpand = true; + element.addEventListener('open', open); + await elementIsStable(element); + + await emulateClick(trigger); + expect(open).not.toHaveBeenCalled(); + expect(element.expanded).toBe(false); + expect(trigger.disabled).toBe(true); + }); + + it('should support open, close, and toggle commands', async () => { + element.dispatchEvent(new CommandEvent('command', { command: '--open' })); + await elementIsStable(element); + expect(element.expanded).toBe(true); + + element.dispatchEvent(new CommandEvent('command', { command: '--close' })); + await elementIsStable(element); + expect(element.expanded).toBe(false); + + element.dispatchEvent(new CommandEvent('command', { command: '--toggle' })); + await elementIsStable(element); + expect(element.expanded).toBe(true); + }); + + it('should point the indicator toward the disclosed content', async () => { + const indicator = element.shadowRoot.querySelector('nve-icon'); + expect(indicator.direction).toBe('right'); + + element.expanded = true; + await elementIsStable(element); + expect(indicator.direction).toBe('down'); + }); +}); diff --git a/projects/core/src/menu/menu-group.test.visual.ts b/projects/core/src/menu/menu-group.test.visual.ts new file mode 100644 index 0000000000..d459531f44 --- /dev/null +++ b/projects/core/src/menu/menu-group.test.visual.ts @@ -0,0 +1,53 @@ +// 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('menu-group visual', () => { + test('menu-group should match visual baseline', async () => { + const report = await visualRunner.render('menu-group', template()); + expect(report.maxDiffPercentage).toBeLessThan(1); + }); + + test('menu-group should match visual baseline dark theme', async () => { + const report = await visualRunner.render('menu-group.dark', template('dark')); + expect(report.maxDiffPercentage).toBeLessThan(1); + }); +}); + +function template(theme: '' | 'dark' = '') { + return /* html */ ` + +
+ + + •︎•︎•︎•︎•︎•︎ + + + •︎•︎•︎•︎ + + + + + •︎•︎•︎•︎•︎•︎ + + + •︎•︎•︎•︎ + •︎•︎•︎•︎ + + + + •︎•︎•︎•︎•︎•︎ + + •︎•︎•︎•︎ + + +
+ `; +} diff --git a/projects/core/src/menu/menu-group.ts b/projects/core/src/menu/menu-group.ts new file mode 100644 index 0000000000..7b061d4c5e --- /dev/null +++ b/projects/core/src/menu/menu-group.ts @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html, LitElement } from 'lit'; +import { property } from 'lit/decorators/property.js'; +import { + attachInternals, + audit, + scopedRegistry, + TypeExpandableController, + useStyles +} from '@nvidia-elements/core/internal'; +import { Icon } from '@nvidia-elements/core/icon'; +import styles from './menu-group.css?inline'; + +/** + * @element nve-menu-group + * @description Organizes related menu items under a disclosure header that can show or hide one child menu. + * @documentation https://nvidia.github.io/elements/docs/elements/menu/ + * @since 0.0.0 + * @entrypoint \@nvidia-elements/core/menu + * @command --open - Opens the menu group. + * @command --close - Closes the menu group. + * @command --toggle - Toggles the menu group. + * @event open - Dispatched when the user requests that the menu group open. + * @event close - Dispatched when the user requests that the menu group close. + * @slot - Label content for the disclosure header. + * @slot prefix - Non-interactive content before the header label, such as an icon or avatar. + * @slot suffix - Non-interactive content after the header label, such as a badge or count. + * @slot menu - One `nve-menu` containing the grouped menu items. Direct menus use this slot automatically in browsers; server-rendered menus must set `slot="menu"` explicitly. + * @cssprop --background + * @cssprop --font-size + * @cssprop --padding + * @cssprop --gap + * @cssprop --border-radius + * @cssprop --color + * @cssprop --width + * @cssprop --min-height + * @aria https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/ + * @stable false + */ +@audit() +@scopedRegistry() +export class MenuGroup extends LitElement { + static styles = useStyles([styles]); + + static readonly metadata = { + tag: 'nve-menu-group', + version: '0.0.0', + children: ['nve-menu'] + }; + + static elementDefinitions = { + [Icon.metadata.tag]: Icon + }; + + /** Determines whether the child menu is visible. */ + @property({ type: Boolean, reflect: true }) expanded = false; + + /** Prevents user activation of the disclosure header. */ + @property({ type: Boolean, reflect: true }) disabled = false; + + /** Determines whether user activation automatically updates the expanded state. */ + @property({ type: Boolean, attribute: 'behavior-expand' }) behaviorExpand = false; + + /** @private */ + declare _internals: ElementInternals; + + #typeExpandableController = new TypeExpandableController(this); + + render() { + return html` +
+ + +
+ `; + } + + connectedCallback() { + super.connectedCallback(); + attachInternals(this); + this._internals.role = 'group'; + } + + #toggle = () => { + if (!this.disabled) { + this.#typeExpandableController.toggle(); + } + }; +} diff --git a/projects/core/src/menu/menu-item.css b/projects/core/src/menu/menu-item.css index cf6cd04db0..fbd4254485 100644 --- a/projects/core/src/menu/menu-item.css +++ b/projects/core/src/menu/menu-item.css @@ -118,6 +118,10 @@ pointer-events: none; } +::slotted(nve-icon) { + cursor: var(--cursor); +} + ::slotted(nve-divider) { margin: 0 calc(-1 * var(--padding)); } diff --git a/projects/core/src/menu/menu-item.test.ts b/projects/core/src/menu/menu-item.test.ts index b7c9b143f5..ab5647c76e 100644 --- a/projects/core/src/menu/menu-item.test.ts +++ b/projects/core/src/menu/menu-item.test.ts @@ -48,7 +48,26 @@ describe(MenuItem.metadata.tag, () => { expect(element.tabIndex).toBe(-1); }); - it('should provide suffix slot', async () => { - expect(element.shadowRoot.querySelector('slot[name="suffix"]')).toBeTruthy(); + it.each(['prefix', 'suffix'])('should assign content to the %s slot', async name => { + const content = document.createElement('span'); + content.slot = name; + content.textContent = name; + element.append(content); + await elementIsStable(element); + + const slot = element.shadowRoot.querySelector(`slot[name="${name}"]`); + const labelSlot = element.shadowRoot.querySelector('slot:not([name])'); + expect(slot.assignedElements()).toEqual([content]); + expect( + labelSlot + .assignedNodes() + .map(node => node.textContent) + .join('') + ).toBe('item 1'); + + content.remove(); + await elementIsStable(element); + + expect(slot.assignedElements()).toEqual([]); }); }); diff --git a/projects/core/src/menu/menu-item.ts b/projects/core/src/menu/menu-item.ts index 0d1c84f9cb..153dc151c5 100644 --- a/projects/core/src/menu/menu-item.ts +++ b/projects/core/src/menu/menu-item.ts @@ -14,6 +14,7 @@ import styles from './menu-item.css?inline'; * @since 0.11.0 * @entrypoint \@nvidia-elements/core/menu * @slot - Label and supporting content for the menu option. + * @slot prefix - slot for prefix icon * @slot suffix - slot for suffix icon * @cssprop --background * @cssprop --border-radius @@ -50,8 +51,9 @@ export class MenuItem extends ButtonFormControlMixin(LitElement) { render() { return html`
- - + + +
`; } diff --git a/projects/core/src/menu/menu.examples.ts b/projects/core/src/menu/menu.examples.ts index 99f2fd75c0..2da2d97aff 100644 --- a/projects/core/src/menu/menu.examples.ts +++ b/projects/core/src/menu/menu.examples.ts @@ -3,6 +3,7 @@ import { html } from 'lit'; import '@nvidia-elements/core/button/define.js'; +import '@nvidia-elements/core/dot/define.js'; import '@nvidia-elements/core/dropdown/define.js'; import '@nvidia-elements/core/icon/define.js'; import '@nvidia-elements/core/menu/define.js'; @@ -15,7 +16,7 @@ import '@nvidia-elements/core/page/define.js'; export default { title: 'Elements/Menu', - component: 'nve-menu', + component: 'nve-menu' }; /** @@ -32,6 +33,77 @@ export const Default = { ` }; +/** + * @summary Menu items with content slots for icons, dots, and other elements. + * @tags test-case + */ +export const ContentSlots = { + render: () => html` + + Code + Lint + CLI + Profile + Access + + ` +}; + +/** + * @summary Dynamically toggle the compact state of a menu. + * @tags test-case + */ +export const Compact = { + render: () => html` + + + + + + Code + Lint + CLI + Profile + Access + + + ` +}; + +/** + * @summary An explicitly expanded group keeps navigation state controlled by the application without enabling automatic state changes. Use when the application owns expansion state. + */ +export const ExpandedNavigation = { + render: () => html` + + Resources + + Documentation + Examples + + + ` +}; + +/** + * @summary A disabled menu-group header preserves its current expanded content while preventing user toggling. Use when group availability depends on application state. + */ +export const UnavailableNavigation = { + render: () => html` + + Provisioning + + Clusters + Storage + + + ` +}; + /** * @summary Menu with keyboard navigation and ARIA disclosure pattern inside a dropdown. Use when menu items need accessible focus management and arrow key navigation. * @tags pattern @@ -109,30 +181,16 @@ export const Disabled = { ` }; -/** - * @summary Menu items with icons to add visual context and improve usability. - */ -export const Icons = { - render: () => html` - - profile - settings - favorites - logout - - ` -}; - /** * @summary Menu items with links for navigation functionality within menu structures. */ export const Links = { render: () => html` - profile - settings - favorites - logout + profile + settings + favorites + logout ` }; @@ -143,7 +201,7 @@ export const Links = { export const Suffix = { render: () => html` - CMD + C + CopyCMD + C ` }; @@ -195,6 +253,75 @@ export const Complex = { ` }; +/** + * @summary Vertical navigation menu groups with and without icons. For deeply nested navigation use the `nve-tree` component. + */ +export const VerticalNavigation = { + render: () => html` +
+ + + +
+ ` +}; + /** * @summary Use a navigation drawer to overlay page content for out-of-context navigation. * @tags pattern @@ -203,7 +330,7 @@ export const VerticalNavigationDrawer = { render: () => html` - NV + NV

NVIDIA

@@ -214,12 +341,33 @@ export const VerticalNavigationDrawer = {

Drawer

- - item 1 - item 2 - item 3 - item 4 - + + Overview + + Getting Started + Alerts + Create New + + + + Data + + Catalog + Dashboards + Volumes + Models + + + + Develop + + Queries + Notebooks + Pipelines + Experiments + Runs + + @@ -234,20 +382,41 @@ export const VerticalNavigationPanel = { render: () => html` - NV + NV

NVIDIA

- +

Drawer

- - item 1 - item 2 - item 3 - item 4 - + + Overview + + Getting Started + Alerts + Create New + + + + Data + + Catalog + Dashboards + Volumes + Models + + + + Develop + + Queries + Notebooks + Pipelines + Experiments + Runs + +
@@ -273,7 +442,7 @@ export const ItemTooltip = { item 3 ` -} +}; /** * @summary Menu items with danger status styling for destructive actions like delete or logout operations. @@ -289,4 +458,4 @@ export const DangerStatus = { icon right ` -} +}; diff --git a/projects/core/src/menu/menu.global.css b/projects/core/src/menu/menu.global.css index 9732b57fd9..f7d45c681c 100644 --- a/projects/core/src/menu/menu.global.css +++ b/projects/core/src/menu/menu.global.css @@ -9,3 +9,17 @@ nve-menu:has(nve-tooltip), nve-menu:has(nve-tooltip) nve-menu-item { content-visibility: initial; } + +nve-menu[density='compact'] { + --min-width: 0; + + nve-menu-item { + --gap: 0; + width: fit-content; + } + + nve-menu-item::part(_slot), + nve-menu-item::part(_suffix) { + display: none !important; + } +} diff --git a/projects/core/src/menu/menu.test.lighthouse.ts b/projects/core/src/menu/menu.test.lighthouse.ts index 0a50e54f72..b71f24bf21 100644 --- a/projects/core/src/menu/menu.test.lighthouse.ts +++ b/projects/core/src/menu/menu.test.lighthouse.ts @@ -24,6 +24,6 @@ describe('menu lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(16.33); + expect(report.payload.javascript.kb).toBeLessThan(23.5); }); }); diff --git a/projects/core/src/menu/menu.test.ts b/projects/core/src/menu/menu.test.ts index 7fdc36006e..7cd3b46501 100644 --- a/projects/core/src/menu/menu.test.ts +++ b/projects/core/src/menu/menu.test.ts @@ -36,6 +36,35 @@ describe(Menu.metadata.tag, () => { expect(element._internals.role).toBe('menu'); }); + it('should leave density unset by default', () => { + expect(element.density).toBeUndefined(); + expect(element.hasAttribute('density')).toBe(false); + }); + + it.each(['compact', 'default'] as const)('should reflect density %s to an attribute', async density => { + element.density = density; + await elementIsStable(element); + + expect(element.getAttribute('density')).toBe(density); + }); + + it.each(['compact', 'default'] as const)('should update density from attribute %s', async density => { + element.setAttribute('density', density); + await elementIsStable(element); + + expect(element.density).toBe(density); + }); + + it('should remove the density attribute when the property is cleared', async () => { + element.density = 'compact'; + await elementIsStable(element); + + element.density = undefined; + await elementIsStable(element); + + expect(element.hasAttribute('density')).toBe(false); + }); + it('should navigate between items with ArrowDown key', async () => { await elementIsStable(element); const items = element.querySelectorAll('nve-menu-item'); @@ -71,6 +100,117 @@ describe(Menu.metadata.tag, () => { }); }); +describe(`${Menu.metadata.tag}: density and item slots`, () => { + let fixture: HTMLElement; + let element: Menu; + let prefixSlot: HTMLSlotElement; + let labelSlot: HTMLSlotElement; + let suffixSlot: HTMLSlotElement; + + beforeEach(async () => { + fixture = await createFixture(html` + + + prefix + label + suffix + + + `); + element = fixture.querySelector(Menu.metadata.tag); + await elementIsStable(element); + const item = element.querySelector('nve-menu-item'); + await elementIsStable(item); + prefixSlot = item.shadowRoot.querySelector('slot[name="prefix"]'); + labelSlot = item.shadowRoot.querySelector('slot:not([name])'); + suffixSlot = item.shadowRoot.querySelector('slot[name="suffix"]'); + }); + + afterEach(() => { + removeFixture(fixture); + }); + + it('should display the prefix, label, and suffix when density is unset', () => { + expect(getComputedStyle(prefixSlot).display).not.toBe('none'); + expect(getComputedStyle(labelSlot).display).not.toBe('none'); + expect(getComputedStyle(suffixSlot).display).not.toBe('none'); + }); + + it('should hide the label and suffix while keeping the prefix visible in compact density', async () => { + element.density = 'compact'; + await elementIsStable(element); + + expect(getComputedStyle(prefixSlot).display).not.toBe('none'); + expect(getComputedStyle(labelSlot).display).toBe('none'); + expect(getComputedStyle(suffixSlot).display).toBe('none'); + }); + + it.each(['default', undefined] as const)( + 'should restore the label and suffix when density becomes %s', + async density => { + element.density = 'compact'; + await elementIsStable(element); + expect(getComputedStyle(labelSlot).display).toBe('none'); + expect(getComputedStyle(suffixSlot).display).toBe('none'); + + element.density = density; + await elementIsStable(element); + + expect(getComputedStyle(prefixSlot).display).not.toBe('none'); + expect(getComputedStyle(labelSlot).display).not.toBe('none'); + expect(getComputedStyle(suffixSlot).display).not.toBe('none'); + } + ); +}); + +describe(`${Menu.metadata.tag}: menu group slot`, () => { + let fixture: HTMLElement; + let element: Menu; + + beforeEach(async () => { + fixture = await createFixture(html` + + item + + `); + element = fixture.querySelector(Menu.metadata.tag); + await elementIsStable(element); + }); + + afterEach(() => { + removeFixture(fixture); + }); + + it('should clear an automatically assigned menu slot on disconnect', () => { + expect(element.slot).toBe('menu'); + element.remove(); + expect(element.slot).toBe(''); + + fixture.querySelector('nve-menu-group').append(element); + expect(element.slot).toBe('menu'); + element.remove(); + expect(element.slot).toBe(''); + }); + + it('should preserve an authored menu slot on disconnect', () => { + element.remove(); + const authoredGroup = document.createElement('nve-menu-group'); + element = document.createElement(Menu.metadata.tag); + element.slot = 'menu'; + authoredGroup.append(element); + fixture.append(authoredGroup); + + element.remove(); + expect(element.slot).toBe('menu'); + }); + + it('should preserve a changed slot on disconnect', () => { + element.slot = 'other'; + element.remove(); + expect(element.slot).toBe('other'); + }); +}); + describe(`${Menu.metadata.tag}: scroll event`, () => { let fixture: HTMLElement; let element: Menu; diff --git a/projects/core/src/menu/menu.ts b/projects/core/src/menu/menu.ts index 137dc76be5..1504f51241 100644 --- a/projects/core/src/menu/menu.ts +++ b/projects/core/src/menu/menu.ts @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import { html, LitElement } from 'lit'; +import { property } from 'lit/decorators/property.js'; import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; -import type { KeynavListConfig } from '@nvidia-elements/core/internal'; +import type { Density, KeynavListConfig } from '@nvidia-elements/core/internal'; import { useStyles, attachInternals, @@ -44,6 +45,11 @@ export class Menu extends LitElement { static elementDefinitions = {}; + /** + * Determines if the menu is in a compact layout. This hides all menu item content except the prefix icon slot. + */ + @property({ type: String, reflect: true }) density?: Density; + /** @private */ get keynavListConfig(): KeynavListConfig { return { @@ -58,6 +64,7 @@ export class Menu extends LitElement { @queryAssignedElements() private items!: MenuItem[]; #scrollRAF: number | null = null; + #assignedMenuSlot = false; #handleScroll = () => { if (this.#scrollRAF !== null) return; @@ -96,5 +103,18 @@ export class Menu extends LitElement { attachInternals(this); this._internals.role = 'menu'; appendRootNodeStyle(this, globalStyles); + + if (this.parentElement?.localName === 'nve-menu-group' && !this.slot) { + this.slot = 'menu'; + this.#assignedMenuSlot = true; + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + if (this.#assignedMenuSlot && this.slot === 'menu') { + this.slot = ''; + } + this.#assignedMenuSlot = false; } } diff --git a/projects/core/src/select/select.test.lighthouse.ts b/projects/core/src/select/select.test.lighthouse.ts index 4fbb23edcf..82e1b40408 100644 --- a/projects/core/src/select/select.test.lighthouse.ts +++ b/projects/core/src/select/select.test.lighthouse.ts @@ -26,6 +26,6 @@ describe('select lighthouse report', () => { expect(report.scores.performance).toBe(100); expect(report.scores.accessibility).toBe(100); expect(report.scores.bestPractices).toBe(100); - expect(report.payload.javascript.kb).toBeLessThan(35.31); + expect(report.payload.javascript.kb).toBeLessThan(35.6); }); }); diff --git a/projects/core/src/steps/steps.ts b/projects/core/src/steps/steps.ts index 1f4a1ddf75..f552df3d7c 100644 --- a/projects/core/src/steps/steps.ts +++ b/projects/core/src/steps/steps.ts @@ -8,7 +8,7 @@ import { state } from 'lit/decorators/state.js'; import { when } from 'lit/directives/when.js'; import { queryAssignedElements } from 'lit/decorators/query-assigned-elements.js'; import { ButtonFormControlMixin } from '@nvidia-elements/forms/mixins'; -import type { KeynavListConfig, Container } from '@nvidia-elements/core/internal'; +import type { KeynavListConfig } from '@nvidia-elements/core/internal'; import { stateSelected, useStyles, @@ -58,7 +58,7 @@ export class StepsItem extends ButtonFormControlMixin(LitElement) { /** * Determines whether the steps should display in condensed format with no text labels. */ - @property({ type: String, reflect: true }) container?: Extract; + @property({ type: String, reflect: true }) container?: 'condensed'; /** @private */ @state() index: number = 0; @@ -122,7 +122,7 @@ export class Steps extends LitElement { /** * Determines whether the steps should display in condensed format with no text labels. */ - @property({ type: String, reflect: true }) container?: Extract; + @property({ type: String, reflect: true }) container?: 'condensed'; /** * Determines whether the steps should handle selection behavior vs. defaults to off. diff --git a/projects/internals/metadata/static/tests.json b/projects/internals/metadata/static/tests.json index 7048a73258..7eede6f5c3 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:003bd0e158bc1fe3dce72b7ef9b0fc7da1d2fbc55a26dcf2b3c3732b979b12d9 -size 2962409 +oid sha256:ce71d876f17db545219fbb9699b717b944774b1839fcc2aae481684d11f84cd4 +size 2947869 diff --git a/projects/lint/src/eslint/internals/slotted-elements.ts b/projects/lint/src/eslint/internals/slotted-elements.ts index e69686ef82..68b41befa2 100644 --- a/projects/lint/src/eslint/internals/slotted-elements.ts +++ b/projects/lint/src/eslint/internals/slotted-elements.ts @@ -23,6 +23,7 @@ export const SLOTTED_ELEMENT_CONTRACTS = { 'nve-grid-header': ['nve-grid-column'], 'nve-grid-row': ['nve-grid-cell'], 'nve-menu': ['nve-menu-item', 'nve-divider'], + 'nve-menu-group': ['nve-menu'], 'nve-notification-group': ['nve-notification'], 'nve-radio': ['label', 'input', 'nve-control-message'], 'nve-radio-group': ['label', 'nve-control-message', 'nve-radio'], diff --git a/projects/lint/src/eslint/rules/no-invalid-slotted-elements.test.ts b/projects/lint/src/eslint/rules/no-invalid-slotted-elements.test.ts index b54b268a5d..1bb54b2561 100644 --- a/projects/lint/src/eslint/rules/no-invalid-slotted-elements.test.ts +++ b/projects/lint/src/eslint/rules/no-invalid-slotted-elements.test.ts @@ -133,6 +133,15 @@ describe('noInvalidSlottedElements', () => { } ] }, + { + code: '', + errors: [ + { + messageId: 'invalid-slotted-element', + data: { allowed: 'nve-menu', child: 'nve-menu-item', parent: 'nve-menu-group' } + } + ] + }, { code: '
', errors: [ diff --git a/projects/site/src/docs/elements/menu.md b/projects/site/src/docs/elements/menu.md index 5184d257cb..adc5dd6362 100644 --- a/projects/site/src/docs/elements/menu.md +++ b/projects/site/src/docs/elements/menu.md @@ -4,7 +4,7 @@ description: 'Use NVIDIA Elements menus for accessible navigation and actions with keyboard controls, selected states, links, icons, scrolling, and nested dropdown patterns.', layout: 'docs.11ty.js', tag: 'nve-menu', - associatedElements: ['nve-menu-item'] + associatedElements: ['nve-menu-group', 'nve-menu-item'] } --- @@ -24,19 +24,15 @@ {% example '@nvidia-elements/core/menu/menu.examples.json' 'Current' %} -## Border Background - -{% example '@nvidia-elements/core/menu/menu.examples.json' 'BorderBackground' %} - ## Disabled {% api 'nve-menu-item', 'property', 'disabled' %} {% example '@nvidia-elements/core/menu/menu.examples.json' 'Disabled' %} -## Icons +## Content Slots -{% example '@nvidia-elements/core/menu/menu.examples.json' 'Icons' %} +{% example '@nvidia-elements/core/menu/menu.examples.json' 'ContentSlots' %} ## Scroll @@ -44,7 +40,11 @@ ## Dropdown -{% example '@nvidia-elements/core/menu/menu.examples.json' 'Dropdown' '{ "inline": false, "height": "380px" }' %} +{% example '@nvidia-elements/core/menu/menu.examples.json' 'Dropdown' '{ "inline": false, "height": "400px" }' %} + +## Vertical Navigation + +{% example '@nvidia-elements/core/menu/menu.examples.json' 'VerticalNavigation' %} ## Vertical Navigation Drawer @@ -54,6 +54,10 @@ {% example '@nvidia-elements/core/menu/menu.examples.json' 'VerticalNavigationPanel' %} +## Compact + +{% example '@nvidia-elements/core/menu/menu.examples.json' 'Compact' %} + ## Menu Item Tooltip {% example '@nvidia-elements/core/menu/menu.examples.json' 'ItemTooltip' %} @@ -73,3 +77,11 @@ ## Danger Status {% example '@nvidia-elements/core/menu/menu.examples.json' 'DangerStatus' %} + +## Expanded Navigation + +{% example '@nvidia-elements/core/menu/menu.examples.json' 'ExpandedNavigation' %} + +## Border Background + +{% example '@nvidia-elements/core/menu/menu.examples.json' 'BorderBackground' %} diff --git a/projects/starters/angular/package.json b/projects/starters/angular/package.json index 3d2edd218b..4335966d01 100644 --- a/projects/starters/angular/package.json +++ b/projects/starters/angular/package.json @@ -85,19 +85,19 @@ "@nvidia-elements/monaco": "workspace:*", "@nvidia-elements/styles": "workspace:*", "@nvidia-elements/themes": "workspace:*", - "@angular/common": "22.1.7", - "@angular/compiler": "22.1.7", - "@angular/core": "22.1.7", - "@angular/forms": "22.1.7", - "@angular/platform-browser": "22.1.7", - "@angular/router": "22.1.7", + "@angular/common": "22.2.0", + "@angular/compiler": "22.2.0", + "@angular/core": "22.2.0", + "@angular/forms": "22.2.0", + "@angular/platform-browser": "22.2.0", + "@angular/router": "22.2.0", "rxjs": "7.8.2", "tslib": "2.8.1" }, "devDependencies": { "@angular/build": "22.1.7", "@angular/cli": "22.1.7", - "@angular/compiler-cli": "22.1.7", + "@angular/compiler-cli": "22.2.0", "@nvidia-elements/lint": "workspace:*", "eslint": "catalog:", "jsdom": "30.0.1",