diff --git a/projects/core/.visual/format-bytes.dark.png b/projects/core/.visual/format-bytes.dark.png index 7765296bb..6393e724f 100644 --- a/projects/core/.visual/format-bytes.dark.png +++ b/projects/core/.visual/format-bytes.dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:839759336d5205be122856fcd784e71a1cc0890bd60acd3b93596160764388e1 -size 7217 +oid sha256:4a499db395d5965f9adb0a116b9201e2cc95eca2aefa94f74b69c9c4e769f66f +size 7456 diff --git a/projects/core/.visual/format-bytes.png b/projects/core/.visual/format-bytes.png index c39431495..c892a4e98 100644 --- a/projects/core/.visual/format-bytes.png +++ b/projects/core/.visual/format-bytes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5335be8f45db8e866ef98449d92077ed4d8755a0d819c7bfb2fc216f501a28b -size 7183 +oid sha256:9f6d19339e7c77d0497ae22616ba6dd7184afb602a28b1040079df8f4050fd34 +size 7433 diff --git a/projects/core/.visual/icon-button.dark.png b/projects/core/.visual/icon-button.dark.png index 6e59fd245..8cfa5643c 100644 --- a/projects/core/.visual/icon-button.dark.png +++ b/projects/core/.visual/icon-button.dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2abaf8739100050906bd5d0e0ee11a7aca7a7acbf38ad6e9bd5ab3b713c3c51b -size 11036 +oid sha256:58690539fafdce86b033db23fbf2eaf408a955b3fa0c07c87f16634a5887c084 +size 8919 diff --git a/projects/core/.visual/icon-button.png b/projects/core/.visual/icon-button.png index d76fd1f62..e9c1aa142 100644 --- a/projects/core/.visual/icon-button.png +++ b/projects/core/.visual/icon-button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c814528eeef6c0d3b64e0ae97792fc52f6deb155b8b9f20afc8998f61a684729 -size 10538 +oid sha256:5ccee729f199ec27794d92759807da09ac69ad11c2cf523c2f04ac94ae71ca42 +size 8256 diff --git a/projects/core/.visual/icon.dark.png b/projects/core/.visual/icon.dark.png index df885e7b8..980400018 100644 --- a/projects/core/.visual/icon.dark.png +++ b/projects/core/.visual/icon.dark.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c3a0c7e5ad3e78e392a4db816087dbd1e32656a7f915d6950f61ef3fd67ff5d -size 139592 +oid sha256:ad90aa1b87b7eabdd9b9776dd38a19cbb2eb7a6f24dc68005d96281dd6ae4082 +size 53494 diff --git a/projects/core/.visual/icon.explicit-optical-spacing.png b/projects/core/.visual/icon.explicit-optical-spacing.png index 65ed45a71..a5fb5bb1b 100644 --- a/projects/core/.visual/icon.explicit-optical-spacing.png +++ b/projects/core/.visual/icon.explicit-optical-spacing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78b4c0250fe29242ed9b861d9c64ff8f321917d791029f002866b474de7fdb8e -size 3075 +oid sha256:2dc557c4351cf2a46cb48778ded67c49393ebba3fd72399890519a937cdb5e4f +size 2201 diff --git a/projects/core/.visual/icon.no-ghost-space.png b/projects/core/.visual/icon.no-ghost-space.png index 455744633..efc251269 100644 --- a/projects/core/.visual/icon.no-ghost-space.png +++ b/projects/core/.visual/icon.no-ghost-space.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bbcbddb1ba6987d48b96091fc7d9a56e17c13fdf3497b4deb2928a3d97350dd -size 238794 +oid sha256:ead499838a33f17dfab38bb687fbb21a4d3e81282666dcfabbb93ee32d7cbdf8 +size 241742 diff --git a/projects/core/.visual/icon.png b/projects/core/.visual/icon.png index 4957cc87e..a7022f928 100644 --- a/projects/core/.visual/icon.png +++ b/projects/core/.visual/icon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4daaaa8ec6981dbe0c4cd95fe53527cb2feeabc7aac618df341e9d85f2e606fd -size 137255 +oid sha256:1c63acb70bfd1d7875fa61463ecba16f539fe9f73cb63b6a57de6a8d801b17f7 +size 51468 diff --git a/projects/core/.visual/icon.solid.png b/projects/core/.visual/icon.solid.png index ad12bc5a6..b17a79f0a 100644 --- a/projects/core/.visual/icon.solid.png +++ b/projects/core/.visual/icon.solid.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:691a582ab37b2808bbbeb5934ed8fb370ccc7c835f212fdd70d77a7d4216167b -size 19848 +oid sha256:1104cdccc439203d2257cdc77fb544083a1b9188635072ef71bc8a116b5712b7 +size 11111 diff --git a/projects/core/build/icons.js b/projects/core/build/icons.js index 74e2721f3..b2873cc7a 100644 --- a/projects/core/build/icons.js +++ b/projects/core/build/icons.js @@ -31,6 +31,8 @@ const frequentIcons = [ 'double-chevron' ]; +const preservedAspectRatioIcons = new Set(['split-horizontal', 'split-none', 'split-vertical', 'stop']); + let icons = readIconFiles(); validateIconSVGs(icons); icons = await repairViewBoxScales(icons); @@ -232,167 +234,176 @@ async function repairViewBoxScales(svgs) { const context = await browser.newContext(); const page = await context.newPage(); - const result = await page.evaluate(async icons => { - const graphicsSelector = 'circle, ellipse, image, line, path, polygon, polyline, rect, text, use'; - const maxRasterSize = 4096; - const rasterScale = 64; + const result = await page.evaluate( + async ({ icons, authoredViewBoxIconNames }) => { + const preservedAspectRatioIcons = new Set(authoredViewBoxIconNames); + const graphicsSelector = 'circle, ellipse, image, line, path, polygon, polyline, rect, text, use'; + const maxRasterSize = 4096; + const rasterScale = 64; - function getGeometryBounds(svg) { - const { x, y, width, height } = svg.getBBox(); - return { x, y, width, height }; - } + function getGeometryBounds(svg) { + const { x, y, width, height } = svg.getBBox(); + return { x, y, width, height }; + } - function getSourceViewBox(svg, geometryBounds) { - const { x, y, width, height } = svg.viewBox.baseVal; - if (width > 0 && height > 0) return { x, y, width, height }; + function getSourceViewBox(svg, geometryBounds) { + const { x, y, width, height } = svg.viewBox.baseVal; + if (width > 0 && height > 0) return { x, y, width, height }; - const sourceWidth = Number.parseFloat(svg.getAttribute('width')); - const sourceHeight = Number.parseFloat(svg.getAttribute('height')); - return sourceWidth > 0 && sourceHeight > 0 - ? { x: 0, y: 0, width: sourceWidth, height: sourceHeight } - : geometryBounds; - } + const sourceWidth = Number.parseFloat(svg.getAttribute('width')); + const sourceHeight = Number.parseFloat(svg.getAttribute('height')); + return sourceWidth > 0 && sourceHeight > 0 + ? { x: 0, y: 0, width: sourceWidth, height: sourceHeight } + : geometryBounds; + } - function hasVisibleStroke(svg) { - return [...svg.querySelectorAll(graphicsSelector)].some(element => { - const style = getComputedStyle(element); - return ( - style.display !== 'none' && - style.visibility !== 'hidden' && - style.stroke !== 'none' && - Number.parseFloat(style.strokeWidth) > 0 && - Number.parseFloat(style.strokeOpacity) > 0 - ); - }); - } + function hasVisibleStroke(svg) { + return [...svg.querySelectorAll(graphicsSelector)].some(element => { + const style = getComputedStyle(element); + return ( + style.display !== 'none' && + style.visibility !== 'hidden' && + style.stroke !== 'none' && + Number.parseFloat(style.strokeWidth) > 0 && + Number.parseFloat(style.strokeOpacity) > 0 + ); + }); + } - async function getPaintedBounds(svg, sourceViewBox) { - const scale = Math.min(rasterScale, maxRasterSize / Math.max(sourceViewBox.width, sourceViewBox.height)); - const width = Math.max(1, Math.ceil(sourceViewBox.width * scale)); - const height = Math.max(1, Math.ceil(sourceViewBox.height * scale)); - const scaleX = width / sourceViewBox.width; - const scaleY = height / sourceViewBox.height; - const clone = svg.cloneNode(true); - - clone.setAttribute('width', width); - clone.setAttribute('height', height); - clone.setAttribute('preserveAspectRatio', 'none'); - clone.style.setProperty('color', '#000'); - - const url = URL.createObjectURL(new Blob([clone.outerHTML], { type: 'image/svg+xml' })); - const image = new Image(); - image.src = url; - - try { - await image.decode(); - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const context = canvas.getContext('2d', { willReadFrequently: true }); - context.drawImage(image, 0, 0, width, height); - const pixels = context.getImageData(0, 0, width, height).data; - let maxAlpha = 0; - for (let pixel = 0; pixel < width * height; pixel++) { - maxAlpha = Math.max(maxAlpha, pixels[pixel * 4 + 3]); - } - if (maxAlpha === 0) return null; - - // Ignore low-alpha antialiasing fringes while retaining relative opacity for faint icons. - const alphaThreshold = maxAlpha * 0.25; - let xMin = width; - let xMax = -1; - let yMin = height; - let yMax = -1; - - for (let pixel = 0; pixel < width * height; pixel++) { - if (pixels[pixel * 4 + 3] < alphaThreshold) continue; - const x = pixel % width; - const y = Math.floor(pixel / width); - xMin = Math.min(xMin, x); - xMax = Math.max(xMax, x); - yMin = Math.min(yMin, y); - yMax = Math.max(yMax, y); + async function getPaintedBounds(svg, sourceViewBox) { + const scale = Math.min(rasterScale, maxRasterSize / Math.max(sourceViewBox.width, sourceViewBox.height)); + const width = Math.max(1, Math.ceil(sourceViewBox.width * scale)); + const height = Math.max(1, Math.ceil(sourceViewBox.height * scale)); + const scaleX = width / sourceViewBox.width; + const scaleY = height / sourceViewBox.height; + const clone = svg.cloneNode(true); + + clone.setAttribute('width', width); + clone.setAttribute('height', height); + clone.setAttribute('preserveAspectRatio', 'none'); + clone.style.setProperty('color', '#000'); + + const url = URL.createObjectURL(new Blob([clone.outerHTML], { type: 'image/svg+xml' })); + const image = new Image(); + image.src = url; + + try { + await image.decode(); + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d', { willReadFrequently: true }); + context.drawImage(image, 0, 0, width, height); + const pixels = context.getImageData(0, 0, width, height).data; + let maxAlpha = 0; + for (let pixel = 0; pixel < width * height; pixel++) { + maxAlpha = Math.max(maxAlpha, pixels[pixel * 4 + 3]); + } + if (maxAlpha === 0) return null; + + // Ignore low-alpha antialiasing fringes while retaining relative opacity for faint icons. + const alphaThreshold = maxAlpha * 0.25; + let xMin = width; + let xMax = -1; + let yMin = height; + let yMax = -1; + + for (let pixel = 0; pixel < width * height; pixel++) { + if (pixels[pixel * 4 + 3] < alphaThreshold) continue; + const x = pixel % width; + const y = Math.floor(pixel / width); + xMin = Math.min(xMin, x); + xMax = Math.max(xMax, x); + yMin = Math.min(yMin, y); + yMax = Math.max(yMax, y); + } + + return xMax < xMin || yMax < yMin + ? null + : { + x: sourceViewBox.x + xMin / scaleX, + y: sourceViewBox.y + yMin / scaleY, + width: (xMax - xMin + 1) / scaleX, + height: (yMax - yMin + 1) / scaleY + }; + } finally { + URL.revokeObjectURL(url); } + } - return xMax < xMin || yMax < yMin - ? null - : { - x: sourceViewBox.x + xMin / scaleX, - y: sourceViewBox.y + yMin / scaleY, - width: (xMax - xMin + 1) / scaleX, - height: (yMax - yMin + 1) / scaleY - }; - } finally { - URL.revokeObjectURL(url); + function formatViewBox(bounds) { + const precision = 100; + const x = Math.floor(bounds.x * precision) / precision; + const y = Math.floor(bounds.y * precision) / precision; + const xMax = Math.ceil((bounds.x + bounds.width) * precision) / precision; + const yMax = Math.ceil((bounds.y + bounds.height) * precision) / precision; + return [x, y, xMax - x, yMax - y].map(value => +value.toFixed(2)).join(' '); } - } - function formatViewBox(bounds) { - const precision = 100; - const x = Math.floor(bounds.x * precision) / precision; - const y = Math.floor(bounds.y * precision) / precision; - const xMax = Math.ceil((bounds.x + bounds.width) * precision) / precision; - const yMax = Math.ceil((bounds.y + bounds.height) * precision) / precision; - return [x, y, xMax - x, yMax - y].map(value => +value.toFixed(2)).join(' '); - } + function fitBoundsToAspectRatio(bounds, source) { + const sourceAspectRatio = source.width / source.height; + const boundsAspectRatio = bounds.width / bounds.height; + let { x, y, width, height } = bounds; + + if (boundsAspectRatio > sourceAspectRatio) { + const fittedHeight = width / sourceAspectRatio; + y -= (fittedHeight - height) / 2; + height = fittedHeight; + } else { + const fittedWidth = height * sourceAspectRatio; + x -= (fittedWidth - width) / 2; + width = fittedWidth; + } - function fitBoundsToAspectRatio(bounds, source) { - const sourceAspectRatio = source.width / source.height; - const boundsAspectRatio = bounds.width / bounds.height; - let { x, y, width, height } = bounds; - - if (boundsAspectRatio > sourceAspectRatio) { - const fittedHeight = width / sourceAspectRatio; - y -= (fittedHeight - height) / 2; - height = fittedHeight; - } else { - const fittedWidth = height * sourceAspectRatio; - x -= (fittedWidth - width) / 2; - width = fittedWidth; + // Keep the fitted bounds inside the source canvas while retaining all painted pixels. + x = Math.min(Math.max(x, source.x), source.x + source.width - width); + y = Math.min(Math.max(y, source.y), source.y + source.height - height); + return { x, y, width, height }; } - // Keep the fitted bounds inside the source canvas while retaining all painted pixels. - x = Math.min(Math.max(x, source.x), source.x + source.width - width); - y = Math.min(Math.max(y, source.y), source.y + source.height - height); - return { x, y, width, height }; - } + function viewBoxesDiffer(source, target) { + const tolerance = 0.025; + return [ + source.x - target.x, + source.y - target.y, + source.x + source.width - (target.x + target.width), + source.y + source.height - (target.y + target.height) + ].some(difference => Math.abs(difference) > tolerance); + } - function viewBoxesDiffer(source, target) { - const tolerance = 0.025; - return [ - source.x - target.x, - source.y - target.y, - source.x + source.width - (target.x + target.width), - source.y + source.height - (target.y + target.height) - ].some(difference => Math.abs(difference) > tolerance); - } + const result = {}; + for (const [name, icon] of Object.entries(icons)) { + if (preservedAspectRatioIcons.has(name)) { + result[name] = icon; + continue; + } - const result = {}; - for (const [name, icon] of Object.entries(icons)) { - const div = document.createElement('div'); - document.body.append(div); - try { - div.innerHTML = icon; - const svg = div.querySelector('svg'); - const geometryBounds = getGeometryBounds(svg); - const sourceViewBox = getSourceViewBox(svg, geometryBounds); - const requiresPaintedBounds = hasVisibleStroke(svg) || svg.querySelector('mask') !== null; - const paintedBounds = requiresPaintedBounds ? await getPaintedBounds(svg, sourceViewBox) : null; - const targetViewBox = fitBoundsToAspectRatio(paintedBounds ?? geometryBounds, sourceViewBox); - // Keep intrinsic SVG bounds tight without changing the source canvas ratio. Intentional optical spacing - // belongs in the icon's presentation styles. - if (!svg.hasAttribute('viewBox') || viewBoxesDiffer(sourceViewBox, targetViewBox)) { - svg.setAttribute('viewBox', formatViewBox(targetViewBox)); + const div = document.createElement('div'); + document.body.append(div); + try { + div.innerHTML = icon; + const svg = div.querySelector('svg'); + const geometryBounds = getGeometryBounds(svg); + const sourceViewBox = getSourceViewBox(svg, geometryBounds); + const requiresPaintedBounds = hasVisibleStroke(svg) || svg.querySelector('mask') !== null; + const paintedBounds = requiresPaintedBounds ? await getPaintedBounds(svg, sourceViewBox) : null; + const targetViewBox = fitBoundsToAspectRatio(paintedBounds ?? geometryBounds, sourceViewBox); + // Keep intrinsic SVG bounds tight without changing the source canvas ratio. Intentional optical spacing + // belongs in the icon's presentation styles. + if (!svg.hasAttribute('viewBox') || viewBoxesDiffer(sourceViewBox, targetViewBox)) { + svg.setAttribute('viewBox', formatViewBox(targetViewBox)); + } + result[name] = div.innerHTML; + } catch (error) { + throw new Error(`Failed to repair viewBox for icon "${name}"`, { cause: error }); + } finally { + div.remove(); } - result[name] = div.innerHTML; - } catch (error) { - throw new Error(`Failed to repair viewBox for icon "${name}"`, { cause: error }); - } finally { - div.remove(); } - } - return result; - }, svgs); + return result; + }, + { icons: svgs, authoredViewBoxIconNames: [...preservedAspectRatioIcons] } + ); await browser.close(); return result; } diff --git a/projects/core/src/dropzone/dropzone.css b/projects/core/src/dropzone/dropzone.css index 52e5faf00..80e859c9f 100644 --- a/projects/core/src/dropzone/dropzone.css +++ b/projects/core/src/dropzone/dropzone.css @@ -31,7 +31,7 @@ user-select: none; } -.border { +.dropzone-border { border-radius: var(--border-radius); color: var(--border-color); position: absolute; @@ -54,17 +54,17 @@ slot[name='icon']::slotted(*) { color: var(--nve-sys-text-emphasis-color); } -.text-center { +.dropzone-text-center { text-align: center; line-height: var(--nve-ref-font-line-height-relaxed); font-size: var(--nve-ref-font-size-200); } -.text-center::first-letter { +.dropzone-text-center::first-letter { text-transform: capitalize; } -.text-bold { +.dropzone-text-bold { font-weight: var(--nve-ref-font-weight-semibold); } diff --git a/projects/core/src/dropzone/dropzone.ts b/projects/core/src/dropzone/dropzone.ts index b004dbed5..3944e352d 100644 --- a/projects/core/src/dropzone/dropzone.ts +++ b/projects/core/src/dropzone/dropzone.ts @@ -178,13 +178,13 @@ export class Dropzone extends FormControlMixin(LitEle @dragover=${this.#handleDragOver} @dragleave=${this.#handleDragLeave} @drop=${this.#handleDrop}> - + -
-
${this.i18n.dragAndDrop} ${this.i18n.files} ${this.i18n.or} ${this.i18n.browseFiles}
+
+
${this.i18n.dragAndDrop} ${this.i18n.files} ${this.i18n.or} ${this.i18n.browseFiles}
${this.#formatFileTypeSpecifiers(this.accept)} — ${this.i18n.maxFileSize} ${formatFileSize(this.maxFileSize)}
diff --git a/projects/core/src/format-bytes/format-bytes.test.ssr.ts b/projects/core/src/format-bytes/format-bytes.test.ssr.ts index 6db4ac007..b6d4e78df 100644 --- a/projects/core/src/format-bytes/format-bytes.test.ssr.ts +++ b/projects/core/src/format-bytes/format-bytes.test.ssr.ts @@ -12,7 +12,7 @@ describe(FormatBytes.metadata.tag, () => { 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('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 index 02f6c8516..c3d20814a 100644 --- a/projects/core/src/format-bytes/format-bytes.test.ts +++ b/projects/core/src/format-bytes/format-bytes.test.ts @@ -40,7 +40,7 @@ describe(FormatBytes.metadata.tag, () => { it('should render semantic data with the raw byte count', () => { expect(renderedData(element)?.getAttribute('value')).toBe('1048576'); - expect(renderedText(element)).toBe('1.05 mb'); + expect(renderedText(element)).toBe('1.05 MB'); }); it('should use value over slot content', async () => { @@ -48,7 +48,7 @@ describe(FormatBytes.metadata.tag, () => { await elementIsStable(element); expect(renderedData(element)?.getAttribute('value')).toBe('1024'); - expect(renderedText(element)).toBe('1.02 kb'); + expect(renderedText(element)).toBe('1.02 kB'); }); it('should use slot content when value attribute is removed', async () => { @@ -59,7 +59,7 @@ describe(FormatBytes.metadata.tag, () => { await elementIsStable(element); expect(renderedData(element)?.getAttribute('value')).toBe('1048576'); - expect(renderedText(element)).toBe('1.05 mb'); + expect(renderedText(element)).toBe('1.05 MB'); }); it('should render empty output without a value', async () => { @@ -74,15 +74,17 @@ describe(FormatBytes.metadata.tag, () => { element.textContent = '1073741824'; await elementIsStable(element); - expect(renderedText(element)).toBe('1.07 gb'); + 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'] + ['999', '999 B'], + ['1000', '1 kB'], + ['1024', '1.02 kB'], + ['1048576', '1.05 MB'], + ['1073741824', '1.07 GB'], + ['1000000000000', '1 TB'], + ['1000000000000000', '1 PB'] ])('should automatically format decimal bytes %s as %s', async (value, expected) => { element.textContent = value; await elementIsStable(element); @@ -91,10 +93,12 @@ describe(FormatBytes.metadata.tag, () => { }); it.each([ - ['1023', '1,023 b'], - ['1024', '1 kib'], - ['1048576', '1 mib'], - ['1073741824', '1 gib'] + ['1023', '1,023 B'], + ['1024', '1 KiB'], + ['1048576', '1 MiB'], + ['1073741824', '1 GiB'], + ['1099511627776', '1 TiB'], + ['1125899906842624', '1 PiB'] ])('should automatically format binary bytes %s as %s', async (value, expected) => { element.display = 'binary'; element.textContent = value; @@ -104,9 +108,9 @@ describe(FormatBytes.metadata.tag, () => { }); it.each<[FormatBytesUnit, string]>([ - ['kb', '1,048.58 kb'], - ['mb', '1.05 mb'], - ['gb', '0 gb'] + ['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); @@ -119,7 +123,7 @@ describe(FormatBytes.metadata.tag, () => { element.unit = 'kb'; await elementIsStable(element); - expect(renderedText(element)).toBe('1,024 kib'); + expect(renderedText(element)).toBe('1,024 KiB'); }); it.each([ @@ -157,7 +161,7 @@ describe(FormatBytes.metadata.tag, () => { element.maximumFractionDigits = 0; await elementIsStable(element); - expect(renderedText(element)).toBe('1 mb'); + expect(renderedText(element)).toBe('1 MB'); }); it('should format with fixed fraction digits', async () => { @@ -166,7 +170,7 @@ describe(FormatBytes.metadata.tag, () => { element.maximumFractionDigits = 3; await elementIsStable(element); - expect(renderedText(element)).toBe('1.235 mb'); + expect(renderedText(element)).toBe('1.235 MB'); }); it('should expand the effective default maximum for minimum fraction digits', async () => { @@ -174,14 +178,14 @@ describe(FormatBytes.metadata.tag, () => { element.minimumFractionDigits = 3; await elementIsStable(element); - expect(renderedText(element)).toBe('1.235 mb'); + 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'); + expect(renderedText(element)).toBe('1,05 MB'); }); it('should use the document locale by default', async () => { @@ -190,7 +194,7 @@ describe(FormatBytes.metadata.tag, () => { element.requestUpdate(); await elementIsStable(element); - expect(renderedText(element)).toBe('1,05 mb'); + expect(renderedText(element)).toBe('1,05 MB'); }); it('should use the runtime locale when the document language is empty', async () => { @@ -200,24 +204,24 @@ describe(FormatBytes.metadata.tag, () => { await elementIsStable(element); const expectedNumber = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(1.05); - expect(renderedText(element)).toBe(`${expectedNumber} mb`); + 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'); + expect(renderedText(element)).toBe('0 B'); element.value = -1000; await elementIsStable(element); - expect(renderedText(element)).toBe('-1 kb'); + 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'); + expect(renderedText(element)).toBe('1,000 PB'); }); it.each([ diff --git a/projects/core/src/format-bytes/format-bytes.ts b/projects/core/src/format-bytes/format-bytes.ts index cdd0943d4..344cf8e8a 100644 --- a/projects/core/src/format-bytes/format-bytes.ts +++ b/projects/core/src/format-bytes/format-bytes.ts @@ -21,21 +21,21 @@ 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' } + 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' } + 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 { @@ -86,7 +86,7 @@ export class FormatBytes extends LitElement { @property({ type: String }) unit?: FormatBytesUnit; /** - * Unit label length: 'short' renders labels such as 'mb'; 'long' renders labels such as 'megabytes'. + * Unit label length: 'short' renders labels such as 'MB'; 'long' renders labels such as 'megabytes'. */ @property({ type: String, attribute: 'unit-display' }) unitDisplay: FormatBytesUnitDisplay = 'short'; diff --git a/projects/core/src/forms/control-message/control-message.ts b/projects/core/src/forms/control-message/control-message.ts index 85c40a76a..36b33a757 100644 --- a/projects/core/src/forms/control-message/control-message.ts +++ b/projects/core/src/forms/control-message/control-message.ts @@ -23,6 +23,7 @@ const statusIcons = { * @since 0.3.0 * @entrypoint \@nvidia-elements/core/forms * @slot - Validation or supporting message text for the associated control. + * @slot icon - override the icon with a custom icon * @cssprop --color * @cssprop --font-weight * @cssprop --font-size diff --git a/projects/core/src/icon/icon.test.visual.ts b/projects/core/src/icon/icon.test.visual.ts index 9b99f2633..64f53af30 100644 --- a/projects/core/src/icon/icon.test.visual.ts +++ b/projects/core/src/icon/icon.test.visual.ts @@ -17,7 +17,18 @@ describe('icon visual', () => { }); // icon.css intentionally scales down these icons - const ICONS_WITH_OPTICAL_SPACING = ['arrow', 'arrow-angle', 'cancel', 'caret', 'chevron', 'double-chevron']; + const ICONS_WITH_OPTICAL_SPACING = [ + 'arrow', + 'arrow-angle', + 'cancel', + 'caret', + 'chevron', + 'double-chevron', + 'split-horizontal', + 'split-none', + 'split-vertical', + 'stop' + ]; test('icon by default should not have any ghost empty space between its intrinsic dimensions and svg viewbox', async () => { const report = await visualRunner.render( diff --git a/projects/core/src/icon/icons/pointer-solid.svg b/projects/core/src/icon/icons/pointer-solid.svg index 27be5de0b..d90256391 100644 --- a/projects/core/src/icon/icons/pointer-solid.svg +++ b/projects/core/src/icon/icons/pointer-solid.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/projects/core/src/icon/icons/split-horizontal.svg b/projects/core/src/icon/icons/split-horizontal.svg index 60045a04c..43000a99f 100644 --- a/projects/core/src/icon/icons/split-horizontal.svg +++ b/projects/core/src/icon/icons/split-horizontal.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/projects/core/src/icon/icons/split-none.svg b/projects/core/src/icon/icons/split-none.svg index 691f8e598..40dd15f5b 100644 --- a/projects/core/src/icon/icons/split-none.svg +++ b/projects/core/src/icon/icons/split-none.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/projects/core/src/icon/icons/split-vertical.svg b/projects/core/src/icon/icons/split-vertical.svg index 2f47da034..89940e27a 100644 --- a/projects/core/src/icon/icons/split-vertical.svg +++ b/projects/core/src/icon/icons/split-vertical.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/projects/core/src/icon/icons/stop.svg b/projects/core/src/icon/icons/stop.svg index c8eb37428..11d3dcd19 100644 --- a/projects/core/src/icon/icons/stop.svg +++ b/projects/core/src/icon/icons/stop.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/projects/core/src/icon/server.ts b/projects/core/src/icon/server.ts index 8e496260d..5457803eb 100644 --- a/projects/core/src/icon/server.ts +++ b/projects/core/src/icon/server.ts @@ -23,7 +23,7 @@ globalThis._NVE_SSR_ICON_REGISTRY = { 'delete': '', 'download': '', 'search': '', - 'split-vertical': '', + 'split-vertical': '', 'sparkles': '', 'branch': '', 'refresh': '', @@ -224,7 +224,7 @@ globalThis._NVE_SSR_ICON_REGISTRY = { 'plus-circle-solid': '', 'plus-minus': '', 'pointer': '', - 'pointer-solid': '', + 'pointer-solid': '', 'priority-high': '', 'priority-low': '', 'priority-medium': '', @@ -259,15 +259,15 @@ globalThis._NVE_SSR_ICON_REGISTRY = { 'sort-descending': '', 'soundwave': '', 'sparkles-solid': '', - 'split-horizontal': '', - 'split-none': '', + 'split-horizontal': '', + 'split-none': '', 'star': '', 'star-half': '', 'star-stroke': '', 'start': '', 'status-offline': '', 'status-online': '', - 'stop': '', + 'stop': '', 'stop-sign': '', 'stopwatch': '', 'strikethrough': '', diff --git a/projects/core/src/index.test.lighthouse.ts b/projects/core/src/index.test.lighthouse.ts index 70de06d19..85aa635ea 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(109.05); + expect(report.payload.javascript.requests['index.js'].kb).toBeLessThan(109.1); // 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); @@ -107,7 +107,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( - 89.56 + 90 ); }); }); diff --git a/projects/core/src/progress-ring/progress-ring.css b/projects/core/src/progress-ring/progress-ring.css index 7538f772e..35dcc02a0 100644 --- a/projects/core/src/progress-ring/progress-ring.css +++ b/projects/core/src/progress-ring/progress-ring.css @@ -65,7 +65,7 @@ circle.background { stroke: none; } -circle.ring { +circle.progress-ring-circle { fill: none; stroke: var(--ring-color); stroke-width: var(--ring-width); @@ -116,7 +116,7 @@ circle.ring { display: none; } -[internal-host][indeterminate] circle.ring { +[internal-host][indeterminate] circle.progress-ring-circle { animation: var(--animation-duration) linear 0s infinite normal none running spin-infinite; } diff --git a/projects/core/src/progress-ring/progress-ring.test.ts b/projects/core/src/progress-ring/progress-ring.test.ts index 096bae292..929117736 100644 --- a/projects/core/src/progress-ring/progress-ring.test.ts +++ b/projects/core/src/progress-ring/progress-ring.test.ts @@ -134,7 +134,7 @@ describe(ProgressRing.metadata.tag, () => { it('should set stroke-dasharray to 0px 44px when value is 0', async () => { element.value = 0; await elementIsStable(element); - const ring = element.shadowRoot.querySelector('.ring') as SVGCircleElement; + const ring = element.shadowRoot.querySelector('.progress-ring-circle') as SVGCircleElement; expect(ring.getAttribute('stroke-dasharray')).toBe('0px 44px'); }); @@ -142,7 +142,7 @@ describe(ProgressRing.metadata.tag, () => { element.value = 150; element.max = 100; await elementIsStable(element); - const ring = element.shadowRoot.querySelector('.ring') as SVGCircleElement; + const ring = element.shadowRoot.querySelector('.progress-ring-circle') as SVGCircleElement; const dashValue = parseFloat(ring.getAttribute('stroke-dasharray')); expect(dashValue).toBeGreaterThan(44); }); @@ -152,7 +152,7 @@ describe(ProgressRing.metadata.tag, () => { element.max = undefined; await elementIsStable(element); - const ring = element.shadowRoot.querySelector('.ring') as SVGCircleElement; + const ring = element.shadowRoot.querySelector('.progress-ring-circle') as SVGCircleElement; expect(ring.getAttribute('stroke-dasharray')).toBe('22px 44px'); }); diff --git a/projects/core/src/progress-ring/progress-ring.ts b/projects/core/src/progress-ring/progress-ring.ts index 6a82e5505..5cb2f71b1 100644 --- a/projects/core/src/progress-ring/progress-ring.ts +++ b/projects/core/src/progress-ring/progress-ring.ts @@ -71,7 +71,7 @@ export class ProgressRing extends LitElement {
-