diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc579f00f..99dd31efc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1914,6 +1914,9 @@ importers: '@vitest/browser': specifier: 'catalog:' version: 4.1.7(vite@8.0.14(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.97.3)(sugarss@5.0.1(postcss@8.5.15))(terser@5.47.1)(yaml@2.8.3))(vitest@4.1.7) + '@vitest/coverage-istanbul': + specifier: 'catalog:' + version: 4.1.7(vitest@4.1.7) cssnano: specifier: 8.0.0 version: 8.0.0(postcss@8.5.15) diff --git a/projects/styles/package.json b/projects/styles/package.json index 3122c9f04..165941edf 100644 --- a/projects/styles/package.json +++ b/projects/styles/package.json @@ -60,6 +60,7 @@ "dev": "wireit", "build": "wireit", "lint": "wireit", + "test": "wireit", "test:lighthouse": "wireit", "test:visual": "wireit" }, @@ -73,6 +74,7 @@ "@nvidia-elements/lint": "workspace:*", "@nvidia-elements/themes": "workspace:*", "@vitest/browser": "catalog:", + "@vitest/coverage-istanbul": "catalog:", "cssnano": "8.0.0", "eslint": "catalog:", "lit": "catalog:", @@ -93,6 +95,8 @@ "lint", "build", "publint", + "test", + "test:lighthouse", "test:visual" ] }, @@ -115,6 +119,7 @@ "command": "NODE_ENV=production vite build && node ./build/metadata.js", "files": [ "src/**", + "!src/**/*.test.ts", "!src/**/*.test.lighthouse.ts", "!src/**/*.test.visual.ts", "package.json", @@ -154,6 +159,23 @@ "NODE_ENV": "production" } }, + "test": { + "command": "vitest run", + "files": [ + "src/**", + "!src/**/*.test.lighthouse.ts", + "!src/**/*.test.visual.ts", + "dist/data.html.json", + "tsconfig.json", + "vite.config.ts", + "vitest.config.ts" + ], + "output": [], + "dependencies": [ + "build", + "../internals/vite:ci" + ] + }, "test:visual": { "command": "vitest run --config=vitest.visual.ts", "clean": false, @@ -177,7 +199,8 @@ }, "lint": { "dependencies": [ - "lint:eslint" + "lint:eslint", + "lint:style" ] }, "lint:eslint": { @@ -192,6 +215,15 @@ "../lint:build" ] }, + "lint:style": { + "command": "stylelint 'src/**/*.css' --config=./stylelint.config.mjs", + "files": [ + "src/**/*.css", + "../../stylelint.config.mjs", + "stylelint.config.mjs" + ], + "output": [] + }, "lint:fix": { "command": "eslint -c ./eslint.config.js --fix", "dependencies": [ diff --git a/projects/styles/src/layout.css b/projects/styles/src/layout.css index 91f176ac0..edaed5959 100644 --- a/projects/styles/src/layout.css +++ b/projects/styles/src/layout.css @@ -35,8 +35,7 @@ /* Horizontal Layout */ &[nve-layout~='row'] { flex-direction: row; - justify-items: flex-start; - align-items: flex-start; + place-items: flex-start; &[nve-layout~='align:left'] { justify-content: flex-start; @@ -130,9 +129,8 @@ /* Centered */ &[nve-layout~='align:center'] { - align-items: center; - align-content: center; - justify-content: center; + place-items: center; + place-content: center; } /* Full Stretch */ @@ -147,17 +145,17 @@ /* Spacing */ &[nve-layout~='align:space-around'] { justify-content: space-around; - gap: none; + gap: var(--nve-ref-space-none); } &[nve-layout~='align:space-between'] { justify-content: space-between; - gap: none; + gap: var(--nve-ref-space-none); } &[nve-layout~='align:space-evenly'] { justify-content: space-evenly; - gap: none; + gap: var(--nve-ref-space-none); } /* Fixed Width Layout Gap Spacing */ @@ -173,20 +171,18 @@ padding: var(--nve-ref-space-$(size)); } - @each $side in top, left, right, bottom { + @each $side, $logical-side in (top, left, right, bottom), (block-start, inline-start, inline-end, block-end) { &[nve-layout~='pad-$(side):$(size)'] { - padding-$(side): var(--nve-ref-space-$(size)); + padding-$(logical-side): var(--nve-ref-space-$(size)); } } &[nve-layout~='pad-x:$(size)'] { - padding-left: var(--nve-ref-space-$(size)); - padding-right: var(--nve-ref-space-$(size)); + padding-inline: var(--nve-ref-space-$(size)); } &[nve-layout~='pad-y:$(size)'] { - padding-top: var(--nve-ref-space-$(size)); - padding-bottom: var(--nve-ref-space-$(size)); + padding-block: var(--nve-ref-space-$(size)); } } @@ -252,18 +248,14 @@ } &[nve-layout~='align:stretch'] { - align-items: stretch; - align-content: stretch; - justify-items: stretch; - justify-content: stretch; + place-items: stretch; + place-content: stretch; @mixin auto-columns; } &[nve-layout~='align:center'] { - align-items: center; - align-content: center; - justify-items: center; - justify-content: center; + place-items: center; + place-content: center; @mixin auto-columns; } } diff --git a/projects/styles/src/metadata.test.ts b/projects/styles/src/metadata.test.ts new file mode 100644 index 000000000..0ddfbffff --- /dev/null +++ b/projects/styles/src/metadata.test.ts @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +interface CustomDataValue { + name: string; +} + +interface GlobalAttribute { + name: string; + values: CustomDataValue[]; +} + +interface HtmlCustomData { + globalAttributes: GlobalAttribute[]; +} + +describe('@nvidia-elements/styles metadata', () => { + it('should generate custom data for public style attributes', () => { + const data = JSON.parse( + readFileSync(resolve(import.meta.dirname, '../dist/data.html.json'), 'utf-8') + ) as HtmlCustomData; + const attributes = new Map( + data.globalAttributes.map(attribute => [attribute.name, new Set(attribute.values.map(value => value.name))]) + ); + + expectAttributeValues(attributes, 'nve-layout', [ + 'row', + 'column', + 'grid', + 'gap:md', + 'pad-x:md', + '@md|row', + '&md|span:6' + ]); + expectAttributeValues(attributes, 'nve-text', ['body', 'heading', 'link', 'truncate']); + expectAttributeValues(attributes, 'nve-display', ['hide', '@md|show', '&md|hide']); + }); +}); + +function expectAttributeValues(attributes: Map>, name: string, values: string[]) { + const attributeValues = attributes.get(name); + + expect(attributeValues).toBeDefined(); + values.forEach(value => expect(attributeValues?.has(value)).toBe(true)); +} diff --git a/projects/styles/src/typography.css b/projects/styles/src/typography.css index c11bf0a47..d208cadc4 100644 --- a/projects/styles/src/typography.css +++ b/projects/styles/src/typography.css @@ -33,18 +33,18 @@ body[nve-text], @supports not (text-box: trim-both cap alphabetic) { /* https://seek-oss.github.io/capsize/ (firefox) */ [nve-text] { - --__capsize-offset: -0.1818em; + --_capsize-offset: -0.1818em; } :is([nve-text~='display'], [nve-text~='heading'], [nve-text~='body'], [nve-text~='label'])::before { content: ''; - margin-bottom: var(--__capsize-offset); + margin-block-end: var(--_capsize-offset); display: table; } :is([nve-text~='display'], [nve-text~='heading'], [nve-text~='body'], [nve-text~='label'])[nve-text]::after { content: ''; - margin-top: var(--__capsize-offset); + margin-block-start: var(--_capsize-offset); display: table; } @@ -55,7 +55,7 @@ body[nve-text], [nve-text~='truncate'] { &::after { - --__capsize-offset: 0; + --_capsize-offset: 0; } } } @@ -132,7 +132,8 @@ body[nve-text], [nve-text~='list'] { margin: 0 !important; - padding: 0 0 0 var(--nve-ref-size-400) !important; + padding-block: 0 !important; + padding-inline: var(--nve-ref-size-400) 0 !important; list-style-position: outside !important; li { @@ -155,7 +156,8 @@ ul[nve-text~='nav'] li { padding: 0; list-style: none !important; color: var(--nve-sys-text-muted-color) !important; - margin: 0 0 var(--nve-ref-space-sm) 0 !important; + margin-block: 0 var(--nve-ref-space-sm) !important; + margin-inline: 0 !important; > a { text-decoration: none !important; @@ -173,7 +175,8 @@ ul[nve-text~='nav'] li { ul[nve-text~='nav'] ul li { font-size: var(--nve-ref-font-size-100); - margin: 0 0 var(--nve-ref-space-sm) var(--nve-ref-space-lg) !important; + margin-block: 0 var(--nve-ref-space-sm) !important; + margin-inline: var(--nve-ref-space-lg) 0 !important; } [nve-text~='link'] { diff --git a/projects/styles/src/view-transitions.css b/projects/styles/src/view-transitions.css index 519b8355d..91b946660 100644 --- a/projects/styles/src/view-transitions.css +++ b/projects/styles/src/view-transitions.css @@ -34,12 +34,14 @@ } } + /* stylelint-disable-next-line keyframes-name-pattern -- underscore prefix marks internal keyframes */ @keyframes _nve-fade-in { from { opacity: 0; } } + /* stylelint-disable-next-line keyframes-name-pattern -- underscore prefix marks internal keyframes */ @keyframes _nve-fade-out { to { opacity: 0; diff --git a/projects/styles/stylelint.config.mjs b/projects/styles/stylelint.config.mjs new file mode 100644 index 000000000..dd0d60b45 --- /dev/null +++ b/projects/styles/stylelint.config.mjs @@ -0,0 +1,18 @@ +import baseConfig from '../../stylelint.config.mjs'; + +/** @type {import("stylelint").Config} */ +export default { + ...baseConfig, + rules: { + ...baseConfig.rules, + 'at-rule-no-unknown': [ + true, + { + ignoreAtRules: ['define-mixin', 'each', 'mixin'] + } + ], + 'at-rule-empty-line-before': null, + 'custom-property-pattern': null, + 'media-query-no-invalid': null + } +}; diff --git a/projects/styles/vitest.config.ts b/projects/styles/vitest.config.ts new file mode 100644 index 000000000..1ce75c0b9 --- /dev/null +++ b/projects/styles/vitest.config.ts @@ -0,0 +1,14 @@ +import { resolve } from 'path'; +import { mergeConfig } from 'vitest/config'; +import { libraryNodeTestConfig } from '@internals/vite/configs/test.node.js'; + +export default mergeConfig(libraryNodeTestConfig, { + root: import.meta.dirname, + resolve: { + alias: { '@nvidia-elements/styles': resolve(import.meta.dirname, './src') } + }, + test: { + include: ['./src/**/*.test.ts'], + setupFiles: [] + } +});