From 57518ef55e24c7972bc798cc7f3a98e6d20dfe40 Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Fri, 31 Jul 2026 17:51:33 -0500 Subject: [PATCH 1/3] feat(core): format-bytes - Introduced `nve-format-bytes` component to convert byte counts into human-readable decimal or binary units Signed-off-by: Cory Rylan --- projects/core/.visual/format-bytes.dark.png | 3 + projects/core/.visual/format-bytes.png | 3 + projects/core/package.json | 12 + projects/core/src/bundle.ts | 2 + projects/core/src/dropdown/dropdown.ts | 6 +- projects/core/src/format-bytes/define.ts | 13 + .../core/src/format-bytes/format-bytes.css | 14 + .../src/format-bytes/format-bytes.examples.ts | 94 ++++++ .../src/format-bytes/format-bytes.test.axe.ts | 32 ++ .../format-bytes.test.lighthouse.ts | 21 ++ .../src/format-bytes/format-bytes.test.ssr.ts | 18 ++ .../src/format-bytes/format-bytes.test.ts | 274 ++++++++++++++++++ .../format-bytes/format-bytes.test.visual.ts | 33 +++ .../core/src/format-bytes/format-bytes.ts | 218 ++++++++++++++ projects/core/src/format-bytes/index.ts | 4 + .../format-datetime.test.lighthouse.ts | 2 +- .../format-number.test.lighthouse.ts | 4 +- .../format-relative-time.test.lighthouse.ts | 2 +- projects/core/src/index.test.lighthouse.ts | 3 +- projects/site/src/_11ty/layouts/common.js | 1 + .../site/src/docs/elements/format-bytes.md | 43 +++ 21 files changed, 794 insertions(+), 8 deletions(-) create mode 100644 projects/core/.visual/format-bytes.dark.png create mode 100644 projects/core/.visual/format-bytes.png create mode 100644 projects/core/src/format-bytes/define.ts create mode 100644 projects/core/src/format-bytes/format-bytes.css create mode 100644 projects/core/src/format-bytes/format-bytes.examples.ts create mode 100644 projects/core/src/format-bytes/format-bytes.test.axe.ts create mode 100644 projects/core/src/format-bytes/format-bytes.test.lighthouse.ts create mode 100644 projects/core/src/format-bytes/format-bytes.test.ssr.ts create mode 100644 projects/core/src/format-bytes/format-bytes.test.ts create mode 100644 projects/core/src/format-bytes/format-bytes.test.visual.ts create mode 100644 projects/core/src/format-bytes/format-bytes.ts create mode 100644 projects/core/src/format-bytes/index.ts create mode 100644 projects/site/src/docs/elements/format-bytes.md diff --git a/projects/core/.visual/format-bytes.dark.png b/projects/core/.visual/format-bytes.dark.png new file mode 100644 index 0000000000..ee24d5f338 --- /dev/null +++ b/projects/core/.visual/format-bytes.dark.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:706ff483db971ca309d32dc42c28afeebe3982e0b965fc137cb44de621a3212d +size 8122 diff --git a/projects/core/.visual/format-bytes.png b/projects/core/.visual/format-bytes.png new file mode 100644 index 0000000000..be082e3b2f --- /dev/null +++ b/projects/core/.visual/format-bytes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4ad01781c3eacad2a5fb1541a346ffb54a9e35437f31c1a98b05e5cafd81ef3 +size 8253 diff --git a/projects/core/package.json b/projects/core/package.json index c6e7a177c4..e785615be7 100644 --- a/projects/core/package.json +++ b/projects/core/package.json @@ -378,6 +378,18 @@ "types": "./dist/file/define.d.ts", "default": "./dist/file/define.js" }, + "./format-bytes": { + "types": "./dist/format-bytes/index.d.ts", + "default": "./dist/format-bytes/index.js" + }, + "./format-bytes/index.js": { + "types": "./dist/format-bytes/index.d.ts", + "default": "./dist/format-bytes/index.js" + }, + "./format-bytes/define.js": { + "types": "./dist/format-bytes/define.d.ts", + "default": "./dist/format-bytes/define.js" + }, "./format-datetime": { "types": "./dist/format-datetime/index.d.ts", "default": "./dist/format-datetime/index.js" diff --git a/projects/core/src/bundle.ts b/projects/core/src/bundle.ts index 8d15c1e5ec..de28bb4e1c 100644 --- a/projects/core/src/bundle.ts +++ b/projects/core/src/bundle.ts @@ -27,6 +27,7 @@ import '@nvidia-elements/core/dropdown/define.js'; import '@nvidia-elements/core/dropdown-group/define.js'; import '@nvidia-elements/core/dropzone/define.js'; import '@nvidia-elements/core/file/define.js'; +import '@nvidia-elements/core/format-bytes/define.js'; import '@nvidia-elements/core/format-datetime/define.js'; import '@nvidia-elements/core/format-number/define.js'; import '@nvidia-elements/core/format-relative-time/define.js'; @@ -96,6 +97,7 @@ export * from '@nvidia-elements/core/dropdown'; export * from '@nvidia-elements/core/dropdown-group'; export * from '@nvidia-elements/core/dropzone'; export * from '@nvidia-elements/core/file'; +export * from '@nvidia-elements/core/format-bytes'; export * from '@nvidia-elements/core/format-datetime'; export * from '@nvidia-elements/core/format-number'; export * from '@nvidia-elements/core/format-relative-time'; diff --git a/projects/core/src/dropdown/dropdown.ts b/projects/core/src/dropdown/dropdown.ts index 1a4f67cc1b..f8bb7cddb4 100644 --- a/projects/core/src/dropdown/dropdown.ts +++ b/projects/core/src/dropdown/dropdown.ts @@ -19,12 +19,12 @@ import styles from './dropdown.css?inline'; /** * @element nve-dropdown - * @description Generic dropdown element for rendering a variety of different content such as interactive navigation or form controls. [MDN Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) + * @description Generic dropdown element for rendering a variety of different content such as interactive navigation or form controls. * @documentation https://nvidia.github.io/elements/docs/elements/dropdown/ * @since 0.6.0 * @entrypoint \@nvidia-elements/core/dropdown - * @event beforetoggle - Dispatched on a popover just before showing or hiding. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/beforetoggle_event) - * @event toggle - Dispatched on a popover element just after showing or hiding. [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/toggle_event) + * @event beforetoggle - Dispatched on a popover just before showing or hiding. + * @event toggle - Dispatched on a popover element just after showing or hiding. * @event open - Dispatched when the dropdown opens. * @event close - Dispatched when the dropdown closes. * @slot - default slot for dropdown content diff --git a/projects/core/src/format-bytes/define.ts b/projects/core/src/format-bytes/define.ts new file mode 100644 index 0000000000..74b3aa8568 --- /dev/null +++ b/projects/core/src/format-bytes/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 { FormatBytes } from '@nvidia-elements/core/format-bytes'; + +define(FormatBytes); + +declare global { + interface HTMLElementTagNameMap { + 'nve-format-bytes': FormatBytes; + } +} diff --git a/projects/core/src/format-bytes/format-bytes.css b/projects/core/src/format-bytes/format-bytes.css new file mode 100644 index 0000000000..5843acc026 --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.css @@ -0,0 +1,14 @@ +/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */ +/* SPDX-License-Identifier: Apache-2.0 */ + +:host { + display: inline; +} + +[internal-host] { + color: var(--nve-sys-text-color, inherit); +} + +slot { + display: none; +} diff --git a/projects/core/src/format-bytes/format-bytes.examples.ts b/projects/core/src/format-bytes/format-bytes.examples.ts new file mode 100644 index 0000000000..d71c4cb1c7 --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.examples.ts @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import '@nvidia-elements/core/format-bytes/define.js'; + +export default { + title: 'Elements/FormatBytes', + component: 'nve-format-bytes' +}; + +/** + * @summary Automatic decimal conversion for concise file sizes and storage metrics. The component selects the appropriate unit from the byte count. + */ +export const Default = { + render: () => html` +
+ 1024 + 1048576 + 1073741824 +
+ ` +}; + +/** + * @summary Forced unit magnitudes for comparing byte counts on a consistent scale. Use when values need the same unit across a table or chart. + */ +export const Unit = { + render: () => html` +
+ 1048576 + 1048576 + 1048576 +
+ ` +}; + +/** + * @summary Short and long unit labels for compact metrics or explanatory text. Long labels improve clarity when space allows. + */ +export const UnitDisplay = { + render: () => html` +
+ 1048576 + 1048576 + 1048576 +
+ ` +}; + +/** + * @summary Fraction digit controls for matching the precision of storage measurements. Use fixed digits when values must align visually. + */ +export const Precision = { + render: () => html` +
+ 1234567 + 1234567 + 1234567 +
+ ` +}; + +/** + * @summary Explicit locale formatting for audiences whose numeric separators differ from the document language. Unit labels remain lowercase English. + */ +export const Locale = { + render: () => html` +
+ 1048576 + 1048576 + 1048576 +
+ ` +}; + +/** + * @summary Value attribute input for JavaScript or bound data. It takes precedence over text content while the text remains an SSR fallback. + */ +export const Value = { + render: () => html`1024` +}; + +/** + * @summary Decimal and binary conversion for matching SI or IEC storage conventions. Use the convention expected by the surrounding product. + */ +export const Display = { + render: () => html` +
+ 1024 + 1024 +
+ ` +}; diff --git a/projects/core/src/format-bytes/format-bytes.test.axe.ts b/projects/core/src/format-bytes/format-bytes.test.axe.ts new file mode 100644 index 0000000000..89c2867a5e --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.test.axe.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { html } from 'lit'; +import { beforeEach, afterEach, describe, expect, it } from 'vitest'; +import { createFixture, elementIsStable, removeFixture } from '@internals/testing'; +import { runAxe } from '@internals/testing/axe'; +import { FormatBytes } from '@nvidia-elements/core/format-bytes'; +import '@nvidia-elements/core/format-bytes/define.js'; + +describe(FormatBytes.metadata.tag, () => { + let fixture: HTMLElement; + let element: FormatBytes; + + beforeEach(async () => { + fixture = await createFixture(html` + 1048576 + `); + element = fixture.querySelector(FormatBytes.metadata.tag); + await elementIsStable(element); + }); + + afterEach(() => { + removeFixture(fixture); + }); + + it('should pass axe check', async () => { + await elementIsStable(element); + const results = await runAxe([FormatBytes.metadata.tag]); + expect(results.violations.length).toBe(0); + }); +}); diff --git a/projects/core/src/format-bytes/format-bytes.test.lighthouse.ts b/projects/core/src/format-bytes/format-bytes.test.lighthouse.ts new file mode 100644 index 0000000000..9cb159a7b2 --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.test.lighthouse.ts @@ -0,0 +1,21 @@ +// 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('format-bytes lighthouse report', () => { + test('format-bytes should meet lighthouse benchmarks', async () => { + const report = await lighthouseRunner.getReport('nve-format-bytes', /* html */ ` + 1048576 + + `); + + expect(report.scores.performance).toBe(100); + expect(report.scores.accessibility).toBe(100); + expect(report.scores.bestPractices).toBe(100); + expect(report.payload.javascript.kb).toBeLessThan(10.5); + }); +}); diff --git a/projects/core/src/format-bytes/format-bytes.test.ssr.ts b/projects/core/src/format-bytes/format-bytes.test.ssr.ts new file mode 100644 index 0000000000..6db4ac007e --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.test.ssr.ts @@ -0,0 +1,18 @@ +// 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 { FormatBytes } from '@nvidia-elements/core/format-bytes'; +import '@nvidia-elements/core/format-bytes/define.js'; + +describe(FormatBytes.metadata.tag, () => { + it('should render formatted semantic output during ssr', async () => { + const result = await ssrRunner.render(html``); + expect(result.includes('shadowroot="open"')).toBe(true); + expect(result.includes('')).toBe(true); + expect(result.includes('1.05 mb')).toBe(true); + expect(result.includes('nve-format-bytes')).toBe(true); + }); +}); diff --git a/projects/core/src/format-bytes/format-bytes.test.ts b/projects/core/src/format-bytes/format-bytes.test.ts new file mode 100644 index 0000000000..02f6c85164 --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.test.ts @@ -0,0 +1,274 @@ +// 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, removeFixture } from '@internals/testing'; +import { FormatBytes, type FormatBytesUnit } from '@nvidia-elements/core/format-bytes'; +import { LogService } from '@nvidia-elements/core/internal'; +import '@nvidia-elements/core/format-bytes/define.js'; + +function renderedData(element: FormatBytes): HTMLDataElement | null { + return element.shadowRoot?.querySelector('data') ?? null; +} + +function renderedText(element: FormatBytes): string { + return renderedData(element)?.textContent?.trim() ?? ''; +} + +describe(FormatBytes.metadata.tag, () => { + let fixture: HTMLElement; + let element: FormatBytes; + let originalDocumentLang: string; + + beforeEach(async () => { + originalDocumentLang = document.documentElement.lang; + fixture = await createFixture(html`1048576`); + element = fixture.querySelector(FormatBytes.metadata.tag); + await elementIsStable(element); + }); + + afterEach(() => { + document.documentElement.lang = originalDocumentLang; + removeFixture(fixture); + vi.restoreAllMocks(); + }); + + it('should define element', () => { + expect(customElements.get(FormatBytes.metadata.tag)).toBeDefined(); + }); + + it('should render semantic data with the raw byte count', () => { + expect(renderedData(element)?.getAttribute('value')).toBe('1048576'); + expect(renderedText(element)).toBe('1.05 mb'); + }); + + it('should use value over slot content', async () => { + element.value = 1024; + await elementIsStable(element); + + expect(renderedData(element)?.getAttribute('value')).toBe('1024'); + expect(renderedText(element)).toBe('1.02 kb'); + }); + + it('should use slot content when value attribute is removed', async () => { + element.setAttribute('value', '1024'); + await elementIsStable(element); + + element.removeAttribute('value'); + await elementIsStable(element); + + expect(renderedData(element)?.getAttribute('value')).toBe('1048576'); + expect(renderedText(element)).toBe('1.05 mb'); + }); + + it('should render empty output without a value', async () => { + element.textContent = ''; + await elementIsStable(element); + + expect(renderedData(element)?.getAttribute('value')).toBe(''); + expect(renderedText(element)).toBe(''); + }); + + it('should re-render when slot content changes', async () => { + element.textContent = '1073741824'; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1.07 gb'); + }); + + it.each([ + ['999', '999 b'], + ['1000', '1 kb'], + ['1024', '1.02 kb'], + ['1048576', '1.05 mb'], + ['1073741824', '1.07 gb'] + ])('should automatically format decimal bytes %s as %s', async (value, expected) => { + element.textContent = value; + await elementIsStable(element); + + expect(renderedText(element)).toBe(expected); + }); + + it.each([ + ['1023', '1,023 b'], + ['1024', '1 kib'], + ['1048576', '1 mib'], + ['1073741824', '1 gib'] + ])('should automatically format binary bytes %s as %s', async (value, expected) => { + element.display = 'binary'; + element.textContent = value; + await elementIsStable(element); + + expect(renderedText(element)).toBe(expected); + }); + + it.each<[FormatBytesUnit, string]>([ + ['kb', '1,048.58 kb'], + ['mb', '1.05 mb'], + ['gb', '0 gb'] + ])('should force the %s unit', async (unit, expected) => { + element.unit = unit; + await elementIsStable(element); + + expect(renderedText(element)).toBe(expected); + }); + + it('should use the forced magnitude with binary labels', async () => { + element.display = 'binary'; + element.unit = 'kb'; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1,024 kib'); + }); + + it.each([ + [1000000, '1 megabyte'], + [1500000, '1.5 megabytes'], + [-1000000, '-1 megabyte'] + ])('should format decimal long labels for %s bytes', async (value, expected) => { + element.value = value; + element.unitDisplay = 'long'; + await elementIsStable(element); + + expect(renderedText(element)).toBe(expected); + }); + + it('should format binary long labels', async () => { + element.display = 'binary'; + element.unitDisplay = 'long'; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1 mebibyte'); + }); + + it('should select a long label from the rounded value', async () => { + element.value = 999999; + element.unit = 'mb'; + element.unitDisplay = 'long'; + element.maximumFractionDigits = 0; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1 megabyte'); + }); + + it('should format with maximum fraction digits', async () => { + element.textContent = '1234567'; + element.maximumFractionDigits = 0; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1 mb'); + }); + + it('should format with fixed fraction digits', async () => { + element.textContent = '1234567'; + element.minimumFractionDigits = 3; + element.maximumFractionDigits = 3; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1.235 mb'); + }); + + it('should expand the effective default maximum for minimum fraction digits', async () => { + element.textContent = '1234567'; + element.minimumFractionDigits = 3; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1.235 mb'); + }); + + it('should use the configured locale', async () => { + element.locale = 'de-DE'; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1,05 mb'); + }); + + it('should use the document locale by default', async () => { + element.locale = undefined; + document.documentElement.lang = 'de-DE'; + element.requestUpdate(); + await elementIsStable(element); + + expect(renderedText(element)).toBe('1,05 mb'); + }); + + it('should use the runtime locale when the document language is empty', async () => { + element.locale = undefined; + document.documentElement.lang = ''; + element.requestUpdate(); + await elementIsStable(element); + + const expectedNumber = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(1.05); + expect(renderedText(element)).toBe(`${expectedNumber} mb`); + }); + + it('should preserve zero and negative values', async () => { + element.value = 0; + await elementIsStable(element); + expect(renderedText(element)).toBe('0 b'); + + element.value = -1000; + await elementIsStable(element); + expect(renderedText(element)).toBe('-1 kb'); + }); + + it('should cap automatic conversion at petabytes', async () => { + element.value = 1e18; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1,000 pb'); + }); + + it.each([ + ['display', 'invalid'], + ['unit', 'invalid'], + ['unit-display', 'invalid'] + ])('should preserve input for an invalid %s option', async (attribute, value) => { + const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined); + element.setAttribute(attribute, value); + await elementIsStable(element); + + expect(renderedText(element)).toBe('1048576'); + expect(warn).toHaveBeenCalledOnce(); + }); + + it('should preserve and warn for invalid numeric input', async () => { + const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined); + element.textContent = 'not-a-number'; + await elementIsStable(element); + + expect(renderedText(element)).toBe('not-a-number'); + expect(warn).toHaveBeenCalledWith('format-bytes: invalid numeric value "not-a-number"'); + }); + + it('should preserve and warn for an invalid value attribute', async () => { + const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined); + element.setAttribute('value', 'not-a-number'); + await elementIsStable(element); + + expect(renderedData(element)?.getAttribute('value')).toBe('not-a-number'); + expect(renderedText(element)).toBe('not-a-number'); + expect(warn).toHaveBeenCalledWith('format-bytes: invalid numeric value "not-a-number"'); + }); + + it('should preserve and warn for a NaN value property', async () => { + const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined); + element.value = Number.NaN; + await elementIsStable(element); + + expect(renderedData(element)?.getAttribute('value')).toBe('NaN'); + expect(renderedText(element)).toBe('NaN'); + expect(warn).toHaveBeenCalledWith('format-bytes: invalid numeric value "NaN"'); + }); + + it('should preserve input for invalid fraction digit options', async () => { + const warn = vi.spyOn(LogService, 'warn').mockImplementation(() => undefined); + element.minimumFractionDigits = 3; + element.maximumFractionDigits = 2; + await elementIsStable(element); + + expect(renderedText(element)).toBe('1048576'); + expect(warn).toHaveBeenCalledOnce(); + }); +}); diff --git a/projects/core/src/format-bytes/format-bytes.test.visual.ts b/projects/core/src/format-bytes/format-bytes.test.visual.ts new file mode 100644 index 0000000000..2507788d75 --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.test.visual.ts @@ -0,0 +1,33 @@ +// 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('format-bytes visual', () => { + test('format-bytes should match visual baseline', async () => { + const report = await visualRunner.render('format-bytes', template()); + expect(report.maxDiffPercentage).toBeLessThan(1); + }); + + test('format-bytes should match visual baseline dark theme', async () => { + const report = await visualRunner.render('format-bytes.dark', template('dark')); + expect(report.maxDiffPercentage).toBeLessThan(1); + }); +}); + +function template(theme: '' | 'dark' = '') { + return /* html */ ` + +
+ 1024 + 1048576 + 1048576 + 1048576 + 1048576 +
+ `; +} diff --git a/projects/core/src/format-bytes/format-bytes.ts b/projects/core/src/format-bytes/format-bytes.ts new file mode 100644 index 0000000000..cdd0943d40 --- /dev/null +++ b/projects/core/src/format-bytes/format-bytes.ts @@ -0,0 +1,218 @@ +// 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 { LogService, typeSSR, useStyles } from '@nvidia-elements/core/internal'; +import styles from './format-bytes.css?inline'; + +export type FormatBytesDisplay = 'decimal' | 'binary'; +export type FormatBytesUnit = 'b' | 'kb' | 'mb' | 'gb' | 'tb' | 'pb'; +export type FormatBytesUnitDisplay = 'short' | 'long'; + +interface UnitLabels { + short: string; + singular: string; + plural: string; +} + +const UNITS: readonly FormatBytesUnit[] = ['b', 'kb', 'mb', 'gb', 'tb', 'pb']; +const DISPLAYS: readonly FormatBytesDisplay[] = ['decimal', 'binary']; +const UNIT_DISPLAYS: readonly FormatBytesUnitDisplay[] = ['short', 'long']; + +const DECIMAL_LABELS: Record = { + b: { short: 'b', singular: 'byte', plural: 'bytes' }, + kb: { short: 'kb', singular: 'kilobyte', plural: 'kilobytes' }, + mb: { short: 'mb', singular: 'megabyte', plural: 'megabytes' }, + gb: { short: 'gb', singular: 'gigabyte', plural: 'gigabytes' }, + tb: { short: 'tb', singular: 'terabyte', plural: 'terabytes' }, + pb: { short: 'pb', singular: 'petabyte', plural: 'petabytes' } +}; + +const BINARY_LABELS: Record = { + b: { short: 'b', singular: 'byte', plural: 'bytes' }, + kb: { short: 'kib', singular: 'kibibyte', plural: 'kibibytes' }, + mb: { short: 'mib', singular: 'mebibyte', plural: 'mebibytes' }, + gb: { short: 'gib', singular: 'gibibyte', plural: 'gibibytes' }, + tb: { short: 'tib', singular: 'tebibyte', plural: 'tebibytes' }, + pb: { short: 'pib', singular: 'pebibyte', plural: 'pebibytes' } +}; + +function isDisplay(value: unknown): value is FormatBytesDisplay { + return DISPLAYS.some(display => display === value); +} + +function isUnit(value: unknown): value is FormatBytesUnit { + return UNITS.some(unit => unit === value); +} + +function isUnitDisplay(value: unknown): value is FormatBytesUnitDisplay { + return UNIT_DISPLAYS.some(unitDisplay => unitDisplay === value); +} + +/** + * @element nve-format-bytes + * @description Formats a byte count as localized, human-readable decimal or binary units. + * @documentation https://nvidia.github.io/elements/docs/elements/format-bytes/ + * @since 0.0.0 + * @entrypoint \@nvidia-elements/core/format-bytes + * @slot - Numeric byte count to format (such as 1048576). Serves as fallback before hydration. + * @aria https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data + */ +@typeSSR() +export class FormatBytes extends LitElement { + static styles = useStyles([styles]); + + static readonly metadata = { + tag: 'nve-format-bytes', + version: '0.0.0' + }; + + /** + * Optional byte count for values supplied by JavaScript or bound data. + * By default, the component formats the element's text content, which also serves as the SSR fallback. + * When both are present, this property takes precedence. + */ + @property({ type: Number }) value?: number; + + /** + * Unit system: 'decimal' uses powers of 1000 and 'binary' uses powers of 1024. + */ + @property({ type: String }) display: FormatBytesDisplay = 'decimal'; + + /** + * Optional unit magnitude. When omitted, the component selects a unit from the byte count. + */ + @property({ type: String }) unit?: FormatBytesUnit; + + /** + * Unit label length: 'short' renders labels such as 'mb'; 'long' renders labels such as 'megabytes'. + */ + @property({ type: String, attribute: 'unit-display' }) unitDisplay: FormatBytesUnitDisplay = 'short'; + + /** + * Language tag (such as en-US or de-DE) used to format the number. + * Defaults to document.documentElement.lang or the runtime default. + */ + @property({ type: String }) locale?: string; + + /** + * Pad fraction output to at least this many digits. + */ + @property({ type: Number, attribute: 'minimum-fraction-digits' }) minimumFractionDigits?: number; + + /** + * Round fraction output to at most this many digits. Defaults to two effective digits after the decimal point. + */ + @property({ type: Number, attribute: 'maximum-fraction-digits' }) maximumFractionDigits?: number; + + get #rawValue(): string { + const value = Number.isNaN(this.value) ? (this.getAttribute('value') ?? this.value) : this.value; + return String(value ?? this.textContent?.trim() ?? ''); + } + + get #resolvedLocale(): string | undefined { + return this.locale ?? (globalThis.document?.documentElement?.lang || undefined); + } + + get #parsedValue(): number | null { + const rawValue = this.#rawValue; + if (!rawValue) return null; + + const numericValue = Number(rawValue); + if (Number.isFinite(numericValue)) return numericValue; + + LogService.warn(`format-bytes: invalid numeric value "${rawValue}"`); + return null; + } + + #resolveAutoUnit(value: number, display: FormatBytesDisplay): FormatBytesUnit { + const base = display === 'binary' ? 1024 : 1000; + const absoluteValue = Math.abs(value); + + for (let index = UNITS.length - 1; index > 0; index--) { + const unit = UNITS[index]; + if (unit && absoluteValue >= base ** index) return unit; + } + + return 'b'; + } + + #formatLabel(unit: FormatBytesUnit, convertedValue: number): string { + const labels = this.display === 'binary' ? BINARY_LABELS[unit] : DECIMAL_LABELS[unit]; + if (this.unitDisplay === 'short') return labels.short; + return Math.abs(convertedValue) === 1 ? labels.singular : labels.plural; + } + + #warnInvalidOption(name: string, value: unknown): void { + LogService.warn(`format-bytes: invalid ${name} value "${String(value)}"`); + } + + #hasValidConfiguration(): boolean { + if (!isDisplay(this.display)) { + this.#warnInvalidOption('display', this.display); + return false; + } + if (!isUnitDisplay(this.unitDisplay)) { + this.#warnInvalidOption('unit-display', this.unitDisplay); + return false; + } + if (this.unit !== undefined && !isUnit(this.unit)) { + this.#warnInvalidOption('unit', this.unit); + return false; + } + return true; + } + + get #numberFormatOptions(): Intl.NumberFormatOptions { + const effectiveMaximumFractionDigits = this.maximumFractionDigits ?? Math.max(this.minimumFractionDigits ?? 0, 2); + return { + minimumFractionDigits: this.minimumFractionDigits, + maximumFractionDigits: effectiveMaximumFractionDigits + }; + } + + #roundNumber(value: number): number { + return Number( + new Intl.NumberFormat('en-US-u-nu-latn', { + ...this.#numberFormatOptions, + useGrouping: false + }).format(value) + ); + } + + #formatNumber(value: number): string { + return new Intl.NumberFormat(this.#resolvedLocale, this.#numberFormatOptions).format(value); + } + + get #formattedBytes(): string { + const rawValue = this.#rawValue; + if (!rawValue) return ''; + + const numericValue = this.#parsedValue; + if (numericValue === null) return rawValue; + if (!this.#hasValidConfiguration()) return rawValue; + + const resolvedUnit = this.unit ?? this.#resolveAutoUnit(numericValue, this.display); + const unitIndex = UNITS.indexOf(resolvedUnit); + const base = this.display === 'binary' ? 1024 : 1000; + const convertedValue = numericValue / base ** unitIndex; + + try { + const roundedValue = this.#roundNumber(convertedValue); + return `${this.#formatNumber(roundedValue)} ${this.#formatLabel(resolvedUnit, roundedValue)}`; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + LogService.warn(`format-bytes: ${message}`); + return rawValue; + } + } + + render() { + return html`${this.#formattedBytes}`; + } + + #onSlotChange() { + this.requestUpdate(); + } +} diff --git a/projects/core/src/format-bytes/index.ts b/projects/core/src/format-bytes/index.ts new file mode 100644 index 0000000000..b315b510e8 --- /dev/null +++ b/projects/core/src/format-bytes/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 './format-bytes.js'; diff --git a/projects/core/src/format-datetime/format-datetime.test.lighthouse.ts b/projects/core/src/format-datetime/format-datetime.test.lighthouse.ts index 412e5af622..f095875970 100644 --- a/projects/core/src/format-datetime/format-datetime.test.lighthouse.ts +++ b/projects/core/src/format-datetime/format-datetime.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('format-datetime 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(12); + expect(report.payload.javascript.kb).toBeLessThan(10); }); }); diff --git a/projects/core/src/format-number/format-number.test.lighthouse.ts b/projects/core/src/format-number/format-number.test.lighthouse.ts index aa40cdf008..74c4b83910 100644 --- a/projects/core/src/format-number/format-number.test.lighthouse.ts +++ b/projects/core/src/format-number/format-number.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('format-number 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(12); + expect(report.payload.javascript.kb).toBeLessThan(10); }); -}); \ No newline at end of file +}); diff --git a/projects/core/src/format-relative-time/format-relative-time.test.lighthouse.ts b/projects/core/src/format-relative-time/format-relative-time.test.lighthouse.ts index 5ad2c34e41..deb7d2ccc7 100644 --- a/projects/core/src/format-relative-time/format-relative-time.test.lighthouse.ts +++ b/projects/core/src/format-relative-time/format-relative-time.test.lighthouse.ts @@ -16,6 +16,6 @@ describe('format-relative-time 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(12); + expect(report.payload.javascript.kb).toBeLessThan(10.5); }); }); diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts index 5e235dbf60..89e1864d76 100644 --- a/projects/core/src/index.test.lighthouse.ts +++ b/projects/core/src/index.test.lighthouse.ts @@ -15,7 +15,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(133.6); + expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(134.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(120); @@ -45,6 +45,7 @@ describe('lighthouse report', () => { import '@nvidia-elements/core/drawer/define.js'; import '@nvidia-elements/core/dropdown/define.js'; import '@nvidia-elements/core/file/define.js'; + import '@nvidia-elements/core/format-bytes/define.js'; import '@nvidia-elements/core/forms/define.js'; import '@nvidia-elements/core/gauge/define.js'; import '@nvidia-elements/core/grid/define.js'; diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js index 588482e1e7..a723383088 100644 --- a/projects/site/src/_11ty/layouts/common.js +++ b/projects/site/src/_11ty/layouts/common.js @@ -272,6 +272,7 @@ export const renderDocsNav = data => /* html */ ` Dropdown Group Dropzone File + Format Bytes Format Datetime Format Number Format Relative Time diff --git a/projects/site/src/docs/elements/format-bytes.md b/projects/site/src/docs/elements/format-bytes.md new file mode 100644 index 0000000000..c502538812 --- /dev/null +++ b/projects/site/src/docs/elements/format-bytes.md @@ -0,0 +1,43 @@ +--- +{ + title: 'Format Bytes', + layout: 'docs.11ty.js', + tag: 'nve-format-bytes' +} +--- + +## Installation + +{% install 'nve-format-bytes' %} + +Format Bytes converts byte counts into readable decimal or binary units. Supply the byte count as text content to provide an SSR fallback, or set the `value` property or attribute for JavaScript and bound data. If both are present, `value` takes precedence. + +The `locale` property controls number formatting. Unit labels remain lowercase English strings in every locale. + +## Default + +{% example '@nvidia-elements/core/format-bytes/format-bytes.examples.json', 'Default' %} + +## Unit + +{% example '@nvidia-elements/core/format-bytes/format-bytes.examples.json', 'Unit' %} + +## Unit Display + +{% example '@nvidia-elements/core/format-bytes/format-bytes.examples.json', 'UnitDisplay' %} + +## Precision + +{% example '@nvidia-elements/core/format-bytes/format-bytes.examples.json', 'Precision' %} + +## Locale + +{% example '@nvidia-elements/core/format-bytes/format-bytes.examples.json', 'Locale' %} + +## Value + +{% example '@nvidia-elements/core/format-bytes/format-bytes.examples.json', 'Value' %} + +## Display + +{% example '@nvidia-elements/core/format-bytes/format-bytes.examples.json', 'Display' %} From b6c9a76987d179cd97fbfe920997f61dafa73d4c Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Wed, 5 Aug 2026 17:45:18 -0500 Subject: [PATCH 2/3] chore(ci): lint rules and skill for component registration and testing - Updated the component creation guide to include new steps for updating shared entry points and measuring payloads. - Modified ESLint rules to enforce bundle registration and added support for Lighthouse benchmark integration. - Expanded Lighthouse test imports to include additional components and adjusted payload size expectations. - Removed deprecated component from vocabulary list and updated related documentation. Signed-off-by: Cory Rylan --- .agents/skills/component-creation/SKILL.md | 7 +- .../config/vocabularies/Elements/accept.txt | 1 - knip.config.js | 1 - mise.toml | 2 +- projects/code/eslint.config.js | 6 + projects/core/eslint.config.js | 12 ++ projects/core/src/index.test.lighthouse.ts | 11 +- .../core/src/internal/utils/keynav.test.ts | 13 +- projects/internals/eslint/README.md | 1 + projects/internals/eslint/src/configs/lit.js | 9 +- .../local/no-missing-bundle-registration.js | 33 +--- .../no-missing-bundle-registration.test.js | 24 +++ .../src/local/no-missing-bundle-test.js | 183 ++++++++++++++++++ .../src/local/no-missing-bundle-test.test.js | 160 +++++++++++++++ projects/internals/eslint/src/local/utils.js | 46 +++-- .../internals/metadata/static/adoption.json | 4 +- .../internals/metadata/static/releases.json | 4 +- projects/internals/metadata/static/tests.json | 4 +- projects/monaco/eslint.config.js | 6 + projects/site/src/docs/elements/index.11ty.js | 9 +- .../site/src/docs/metrics/api-status.11ty.js | 2 +- release.config.js | 3 +- 22 files changed, 475 insertions(+), 66 deletions(-) create mode 100644 projects/internals/eslint/src/local/no-missing-bundle-test.js create mode 100644 projects/internals/eslint/src/local/no-missing-bundle-test.test.js diff --git a/.agents/skills/component-creation/SKILL.md b/.agents/skills/component-creation/SKILL.md index de15f81ac3..874f603a2b 100644 --- a/.agents/skills/component-creation/SKILL.md +++ b/.agents/skills/component-creation/SKILL.md @@ -35,8 +35,11 @@ Read the [component creation guide](projects/site/src/docs/internal/guidelines/c - `component-name.test.lighthouse.ts`:lighthouse tests - `define.ts`:registration using `define()` helper with `HTMLElementTagNameMap` - `index.ts`:side-effect-free export -4. **Update bundle**: add `import '@nvidia-elements/core//define.js'` to `projects/core/src/bundle.ts` in alphabetical order so the bundle registers the component. -5. **Verify**: confirm all files follow the templates in the component creation guide, run `pnpm run lint` and `pnpm run test` from the elements project. +4. **Update shared entry points**: + - Add only `import '@nvidia-elements/core//define.js'` to `projects/core/src/bundle.ts` in alphabetical order. Keep this registration bundle import-only. Export the public API from the component `index.ts` and expose its entry point through the package `exports` map. + - Add the definition import to the `js-modules` list in `projects/core/src/index.test.lighthouse.ts` so the combined direct-import benchmark measures the component. +5. **Measure payloads**: run `mise exec -- pnpm run test:lighthouse` from `projects/core`. Increase a bundle-size limit only when the measured payload exceeds the current limit. +6. **Verify**: confirm all files follow the templates in the component creation guide, then run `mise exec -- pnpm run lint` and `mise exec -- pnpm run test` from `projects/core`. ## References diff --git a/config/vale/styles/config/vocabularies/Elements/accept.txt b/config/vale/styles/config/vocabularies/Elements/accept.txt index 20b0ac8cd7..35733c840c 100644 --- a/config/vale/styles/config/vocabularies/Elements/accept.txt +++ b/config/vale/styles/config/vocabularies/Elements/accept.txt @@ -255,7 +255,6 @@ nve-grid nve-icon nve-icon-button nve-input -nve-json-viewer nve-logo nve-menu nve-month diff --git a/knip.config.js b/knip.config.js index aba86ff867..7b62e4c898 100644 --- a/knip.config.js +++ b/knip.config.js @@ -32,7 +32,6 @@ export default { '@semantic-release/github', '@semantic-release/npm', '@semantic-release/release-notes-generator', - '@typescript-eslint/parser', '@typescript/lib-dom', 'adm-zip', 'archiver', diff --git a/mise.toml b/mise.toml index cd62829f88..bd126af82e 100644 --- a/mise.toml +++ b/mise.toml @@ -20,7 +20,7 @@ run = [ "mise install", "mise exec -- git lfs install --skip-repo", "mise exec -- git lfs pull", - "mise exec -- pnpm i --frozen-lockfile --prefer-offline", + "mise exec -- pnpm i --frozen-lockfile --prefer-offline --config.confirmModulesPurge=false", ] [tasks.setup] diff --git a/projects/code/eslint.config.js b/projects/code/eslint.config.js index 77c1959d58..5d4d793be1 100644 --- a/projects/code/eslint.config.js +++ b/projects/code/eslint.config.js @@ -8,6 +8,12 @@ export default [ ...libraryConfig, ...litConfig, ...jsonConfig, + { + files: ['src/bundle.ts'], + rules: { + 'local/no-missing-bundle-registration': ['error'] + } + }, { files: ['src/**/*.ts'], ignores: ['**/*.test.ts', '**/*.test.*.ts', '**/*.examples.ts'], diff --git a/projects/core/eslint.config.js b/projects/core/eslint.config.js index e0afa8b857..efae7b3c35 100644 --- a/projects/core/eslint.config.js +++ b/projects/core/eslint.config.js @@ -17,6 +17,18 @@ export default [ ...litConfig, ...cssConfig, ...jsonConfig, + { + files: ['src/bundle.ts'], + rules: { + 'local/no-missing-bundle-registration': ['error'], + 'local/no-missing-bundle-test': [ + 'error', + { + lighthouseTestFile: 'index.test.lighthouse.ts' + } + ] + } + }, // Disable no-missing-popover-trigger globally, only enable for examples { rules: { diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts index 89e1864d76..b7d6883ddd 100644 --- a/projects/core/src/index.test.lighthouse.ts +++ b/projects/core/src/index.test.lighthouse.ts @@ -26,7 +26,7 @@ describe('lighthouse report', () => { + \`); + `; + await writeFile(join(sourceDirectory, 'index.test.lighthouse.ts'), source); + } + + return join(sourceDirectory, 'bundle.ts'); +} + +test('defines rule metadata', () => { + assert.equal(noMissingBundleTest.meta.type, 'problem'); + assert.equal(noMissingBundleTest.meta.name, 'no-missing-bundle-test'); + assert.ok(noMissingBundleTest.meta.messages['missing-bundle-test']); + assert.ok(noMissingBundleTest.meta.messages['missing-lighthouse-test']); + assert.deepEqual(noMissingBundleTest.meta.schema[0].required, ['lighthouseTestFile']); +}); + +test('invalid: reports a missing configured Lighthouse test file', async () => { + const filename = await createBundleFixture(); + + tester.run('no-missing-bundle-test', noMissingBundleTest, { + valid: [], + invalid: [ + { + filename, + code: "import '@nvidia-elements/core/button/define.js';", + options: [{ lighthouseTestFile: 'index.test.lighthouse.ts' }], + errors: [ + { + messageId: 'missing-lighthouse-test', + data: { testFile: 'index.test.lighthouse.ts' } + } + ] + } + ] + }); +}); + +test('valid: allows bundle registrations measured by the aggregate Lighthouse test', async () => { + const filename = await createBundleFixture(['button']); + + tester.run('no-missing-bundle-test', noMissingBundleTest, { + valid: [ + { + filename, + code: "import '@nvidia-elements/core/button/define.js';", + options: [{ lighthouseTestFile: 'index.test.lighthouse.ts' }] + } + ], + invalid: [] + }); +}); + +test('invalid: reports bundle registrations missing from the aggregate Lighthouse test', async () => { + const filename = await createBundleFixture(['button']); + + tester.run('no-missing-bundle-test', noMissingBundleTest, { + valid: [], + invalid: [ + { + filename, + code: ` + import '@nvidia-elements/core/button/define.js'; + import '@nvidia-elements/core/card/define.js'; + `, + options: [{ lighthouseTestFile: 'index.test.lighthouse.ts' }], + errors: [ + { + messageId: 'missing-bundle-test', + data: { + component: 'card', + prefix: '@nvidia-elements/core', + testFile: 'index.test.lighthouse.ts' + } + } + ] + } + ] + }); +}); + +test('invalid: ignores matching lighthouse paths in module-script comments and string literals', async () => { + const filename = await createBundleFixture( + [], + ` + const report = lighthouseRunner.getReport('js-modules', /* html */\` + + \`); + ` + ); + + tester.run('no-missing-bundle-test', noMissingBundleTest, { + valid: [], + invalid: [ + { + filename, + code: ` + import '@nvidia-elements/core/button/define.js'; + import '@nvidia-elements/core/card/define.js'; + `, + options: [{ lighthouseTestFile: 'index.test.lighthouse.ts' }], + errors: [ + { + messageId: 'missing-bundle-test', + data: { + component: 'button', + prefix: '@nvidia-elements/core', + testFile: 'index.test.lighthouse.ts' + } + }, + { + messageId: 'missing-bundle-test', + data: { + component: 'card', + prefix: '@nvidia-elements/core', + testFile: 'index.test.lighthouse.ts' + } + } + ] + } + ] + }); +}); diff --git a/projects/internals/eslint/src/local/utils.js b/projects/internals/eslint/src/local/utils.js index 1e389d8e1c..41ba165b28 100644 --- a/projects/internals/eslint/src/local/utils.js +++ b/projects/internals/eslint/src/local/utils.js @@ -1,12 +1,38 @@ -/** - * Shared helpers for the listener/observer/timer cleanup rules. - * Extracted so walker and class-scope logic stay consistent across rules. - */ - -/** - * Recursive AST walker that visits every node under `node`. Safe against the - * `.parent` back-pointer (skipped) and descends into arrays of child nodes. - */ +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +export const DEFAULT_IMPORT_PREFIX = '@nvidia-elements/core'; + +export function getPackageName(startDirectory) { + let directory = startDirectory; + + while (true) { + const packageJsonPath = join(directory, 'package.json'); + if (existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + return typeof packageJson.name === 'string' ? packageJson.name : undefined; + } catch { + return undefined; + } + } + + const parentDirectory = dirname(directory); + if (parentDirectory === directory) return undefined; + directory = parentDirectory; + } +} + +export function getBundleImportPattern(prefix) { + const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`^${escapedPrefix}/([^/]+)/define\\.js$`); +} + +export function getBundleExportPattern(prefix) { + const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(`^${escapedPrefix}/([^/]+)$`); +} + export function walk(node, visit) { if (!node || typeof node !== 'object') { return; @@ -27,12 +53,10 @@ export function walk(node, visit) { } } -/** Collapse internal whitespace so `this.shadowRoot` matches across formatting. */ export function normalize(text) { return text.replace(/\s+/g, ' ').trim(); } -/** Walk up `.parent` looking for the enclosing class. Returns `null` for module-level nodes. */ export function findEnclosingClass(node) { let current = node.parent; while (current) { diff --git a/projects/internals/metadata/static/adoption.json b/projects/internals/metadata/static/adoption.json index defc17c5ec..acd1ebd22d 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:f75da96cd66f2d9555cb9545163b9a0bdbfbefe0b801a1c4261808a41f13745b -size 197852 +oid sha256:d0f4650d1aabb3cdf7a7faa13abc1a3b14016d848854d2e388ccf30993d9a376 +size 201159 diff --git a/projects/internals/metadata/static/releases.json b/projects/internals/metadata/static/releases.json index 5f1906f695..b6f89fa601 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:0788924860cbae954ffdf66591a7d50edd1f8421d94b3ae069ada84cbd4db7c9 -size 19225 +oid sha256:9eddbbb37c1aabdfdc469ea30b62d99955afcf23dd08341297b31ad557256ad3 +size 19515 diff --git a/projects/internals/metadata/static/tests.json b/projects/internals/metadata/static/tests.json index c85f90e0b2..089d8ea4e8 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:e409c35c3a4a4ac4929f8c85b9199f202e24eaf6a1cb9cb886cd121018e44eee -size 2685423 +oid sha256:36a471595a611afdc932ecfd0b56d4353709f4aa66ba552a5e1ab33302c5d739 +size 2709336 diff --git a/projects/monaco/eslint.config.js b/projects/monaco/eslint.config.js index 02a681e31b..4ab83fbdcb 100644 --- a/projects/monaco/eslint.config.js +++ b/projects/monaco/eslint.config.js @@ -8,6 +8,12 @@ export default [ ...libraryConfig, ...litConfig, ...jsonConfig, + { + files: ['src/bundle.ts'], + rules: { + 'local/no-missing-bundle-registration': ['error'] + } + }, { files: ['src/**/*.ts'], ignores: ['**/*.test.ts', '**/*.test.*.ts', '**/*.examples.ts'], diff --git a/projects/site/src/docs/elements/index.11ty.js b/projects/site/src/docs/elements/index.11ty.js index daf296e91c..84b93d6c4b 100644 --- a/projects/site/src/docs/elements/index.11ty.js +++ b/projects/site/src/docs/elements/index.11ty.js @@ -235,7 +235,14 @@ const previewOverrides = { 'nve-monaco-diff-input': renderMonacoMock('nve-monaco-diff-input'), 'nve-monaco-editor': renderMonacoMock('nve-monaco-editor'), 'nve-monaco-input': renderMonacoMock('nve-monaco-input'), - 'nve-monaco-problems': renderMonacoMock('nve-monaco-problems') + 'nve-monaco-problems': renderMonacoMock('nve-monaco-problems'), + 'nve-media-fullscreen-button': /* html */ ``, + 'nve-media-mute-button': /* html */ ``, + 'nve-media-pause-button': /* html */ ``, + 'nve-media-playback-rate-select': /* html */ ``, + 'nve-media-seek-button': /* html */ ``, + 'nve-media-time-range': /* html */ ``, + 'nve-media-volume-range': /* html */ `` }; const getPreviewTemplate = tag => { diff --git a/projects/site/src/docs/metrics/api-status.11ty.js b/projects/site/src/docs/metrics/api-status.11ty.js index 49942e36b1..af3b2dba40 100644 --- a/projects/site/src/docs/metrics/api-status.11ty.js +++ b/projects/site/src/docs/metrics/api-status.11ty.js @@ -31,7 +31,7 @@ const reportDate = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeS new Date(tests.created) ); const elements = siteData.elements - .filter(element => !element.name.includes('internal') && !element.name.includes('json-viewer')) + .filter(element => !element.name.includes('internal')) .sort((a, b) => a.name.localeCompare(b.name)); export function render() { diff --git a/release.config.js b/release.config.js index a046b52850..561b77e887 100644 --- a/release.config.js +++ b/release.config.js @@ -124,7 +124,8 @@ export default { '@semantic-release/github', { successComment: - '🎉 This issue has been resolved in version ${nextRelease.version} 🎉\n\n[Changelog](https://NVIDIA.github.io/elements/docs/changelog/)' + '🎉 This issue has been resolved in version ${nextRelease.version} 🎉\n\n[Changelog](https://NVIDIA.github.io/elements/docs/changelog/)', + releaseBodyTemplate: 'See CHANGELOG.md for details.' } ] ] From a06c5c7ae87ee3456034bab008c51c1288bf1e7c Mon Sep 17 00:00:00 2001 From: Cory Rylan Date: Fri, 7 Aug 2026 14:34:08 -0500 Subject: [PATCH 3/3] fix(cli): update dependencies Signed-off-by: Cory Rylan --- pnpm-lock.yaml | 59 +++++++++------------------------------ projects/cli/package.json | 6 ++-- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b624de69d0..38408f268d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -478,8 +478,8 @@ importers: projects/cli: dependencies: '@inquirer/prompts': - specifier: 8.5.0 - version: 8.5.0(@types/node@25.6.2) + specifier: 8.5.2 + version: 8.5.2(@types/node@25.6.2) '@modelcontextprotocol/ext-apps': specifier: 'catalog:' version: 1.7.5(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) @@ -499,17 +499,17 @@ importers: specifier: 8.0.0 version: 8.0.0 marked: - specifier: 18.0.3 - version: 18.0.3 + specifier: 18.0.9 + version: 18.0.9 marked-terminal: specifier: 7.3.0 - version: 7.3.0(marked@18.0.3) + version: 7.3.0(marked@18.0.9) open: specifier: 11.0.0 version: 11.0.0 ora: - specifier: 9.4.0 - version: 9.4.0 + specifier: 9.4.1 + version: 9.4.1 publint: specifier: 'catalog:' version: 0.3.21 @@ -3477,15 +3477,6 @@ packages: '@types/node': optional: true - '@inquirer/prompts@8.5.0': - resolution: {integrity: sha512-pLjXOnY4y3R1mgyHP3pXD/8eXejp+L/dde/0N2NLKgKfMstqhNZrpvs7Wkzbl9FYFQh10LRQ7QZwq+cz9rrhyw==} - engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} - peerDependencies: - '@types/node': '>=18' - peerDependenciesMeta: - '@types/node': - optional: true - '@inquirer/prompts@8.5.2': resolution: {integrity: sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} @@ -9824,8 +9815,8 @@ packages: engines: {node: '>= 18'} hasBin: true - marked@18.0.3: - resolution: {integrity: sha512-7VT90JOkDeaRWpfjOReRGPEKn0ecdARBkDGL+tT1wZY0efPPqkUxLUSmzy/C7TIylQYJC9STISEsCHrqb/7VIA==} + marked@18.0.9: + resolution: {integrity: sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==} engines: {node: '>= 20'} hasBin: true @@ -10505,10 +10496,6 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - ora@9.4.0: - resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==} - engines: {node: '>=20'} - ora@9.4.1: resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} engines: {node: '>=20'} @@ -12210,10 +12197,6 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string-width@8.2.0: - resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} - engines: {node: '>=20'} - string-width@8.2.1: resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} engines: {node: '>=20'} @@ -15217,7 +15200,7 @@ snapshots: optionalDependencies: '@types/node': 25.9.3 - '@inquirer/prompts@8.5.0(@types/node@25.6.2)': + '@inquirer/prompts@8.5.2(@types/node@25.6.2)': dependencies: '@inquirer/checkbox': 5.2.1(@types/node@25.6.2) '@inquirer/confirm': 6.1.1(@types/node@25.6.2) @@ -22251,14 +22234,14 @@ snapshots: node-emoji: 2.2.0 supports-hyperlinks: 3.2.0 - marked-terminal@7.3.0(marked@18.0.3): + marked-terminal@7.3.0(marked@18.0.9): dependencies: ansi-escapes: 7.3.0 ansi-regex: 6.2.2 chalk: 5.6.2 cli-highlight: 2.1.11 cli-table3: 0.6.5 - marked: 18.0.3 + marked: 18.0.9 node-emoji: 2.2.0 supports-hyperlinks: 3.2.0 @@ -22266,7 +22249,7 @@ snapshots: marked@15.0.12: {} - marked@18.0.3: {} + marked@18.0.9: {} marky@1.3.0: {} @@ -23287,17 +23270,6 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - ora@9.4.0: - dependencies: - chalk: 5.6.2 - cli-cursor: 5.0.0 - cli-spinners: 3.4.0 - is-interactive: 2.0.0 - is-unicode-supported: 2.1.0 - log-symbols: 7.0.1 - stdin-discarder: 0.3.2 - string-width: 8.2.0 - ora@9.4.1: dependencies: chalk: 5.6.2 @@ -25255,11 +25227,6 @@ snapshots: get-east-asian-width: 1.5.0 strip-ansi: 7.2.0 - string-width@8.2.0: - dependencies: - get-east-asian-width: 1.5.0 - strip-ansi: 7.2.0 - string-width@8.2.1: dependencies: get-east-asian-width: 1.5.0 diff --git a/projects/cli/package.json b/projects/cli/package.json index 16f2eb1bc1..ca5d99333d 100644 --- a/projects/cli/package.json +++ b/projects/cli/package.json @@ -49,17 +49,17 @@ "cli:uninstall": "root=\"${NVE_HOME:-$HOME/.nve}\" && rm -f \"$root/bin/nve\" \"$root/manifest.json\" \"$HOME/.local/bin/nve\" && (rmdir \"$root/bin\" \"$root\" 2>/dev/null || true)" }, "dependencies": { - "@inquirer/prompts": "8.5.0", + "@inquirer/prompts": "8.5.2", "@modelcontextprotocol/ext-apps": "catalog:", "@modelcontextprotocol/server": "catalog:", "@nvidia-elements/code": "workspace:*", "@nvidia-elements/lint": "workspace:^", "adm-zip": "0.6.0", "archiver": "8.0.0", - "marked": "18.0.3", + "marked": "18.0.9", "marked-terminal": "7.3.0", "open": "11.0.0", - "ora": "9.4.0", + "ora": "9.4.1", "publint": "catalog:", "yargs": "18.0.0", "zod": "catalog:"