feat(skin): improved responsive design (#1832)

This commit is contained in:
Sam Potts
2026-07-31 06:49:06 +10:00
committed by GitHub
parent 6232247d1a
commit c8a9eecaf8
103 changed files with 2399 additions and 2149 deletions
+27 -7
View File
@@ -17,42 +17,62 @@ export function resolveCSSLength(el: Element, value: string): number {
const parsed = Number.parseFloat(trimmed);
if (Number.isNaN(parsed)) return 0;
if (/^-?\d*\.?\d+$/.test(trimmed) || trimmed.endsWith('px')) return parsed;
if (!Number.isNaN(parsed) && (/^-?\d*\.?\d+$/.test(trimmed) || trimmed.endsWith('px'))) return parsed;
const doc = el.ownerDocument;
const root = doc?.documentElement;
if (trimmed.endsWith('rem')) {
if (!Number.isNaN(parsed) && trimmed.endsWith('rem')) {
const rootFontSize = root ? Number.parseFloat(getComputedStyle(root).fontSize) || 16 : 16;
return parsed * rootFontSize;
}
if (trimmed.endsWith('em')) {
if (!Number.isNaN(parsed) && trimmed.endsWith('em')) {
const fontSize = el instanceof HTMLElement ? Number.parseFloat(getComputedStyle(el).fontSize) || 16 : 16;
return parsed * fontSize;
}
if (!doc) return parsed;
if (!doc) return Number.isNaN(parsed) ? 0 : parsed;
const measurementEl = doc.createElement('div');
measurementEl.style.position = 'absolute';
measurementEl.style.visibility = 'hidden';
measurementEl.style.pointerEvents = 'none';
measurementEl.style.inlineSize = trimmed;
if (!measurementEl.style.inlineSize) return 0;
measurementEl.style.blockSize = '0';
measurementEl.style.padding = '0';
measurementEl.style.border = '0';
measurementEl.style.inset = '0';
const computed = getComputedStyle(el);
measurementEl.style.fontSize = computed.fontSize;
for (let i = 0; i < computed.length; i++) {
const name = computed.item(i);
if (name.startsWith('--')) {
measurementEl.style.setProperty(name, computed.getPropertyValue(name));
}
}
const parent = doc.body ?? doc.documentElement;
if (!parent) return parsed;
if (!parent) return Number.isNaN(parsed) ? 0 : parsed;
parent.appendChild(measurementEl);
if (getComputedStyle(measurementEl).inlineSize === 'auto') {
measurementEl.remove();
return 0;
}
const pixels = measurementEl.getBoundingClientRect().width;
measurementEl.remove();
return Number.isFinite(pixels) ? pixels : parsed;
if (Number.isFinite(pixels)) return pixels;
return Number.isNaN(parsed) ? 0 : parsed;
}
+140 -1
View File
@@ -1,8 +1,12 @@
import { describe, expect, it, vi } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { resolveCSSLength } from '../style';
describe('resolveCSSLength', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('Returns px values directly', () => {
const el = document.createElement('div');
@@ -98,4 +102,139 @@ describe('resolveCSSLength', () => {
createElementSpy.mockRestore();
});
it('Returns zero for invalid CSS lengths', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => ({
x: 0,
y: 0,
width: 640,
height: 0,
top: 0,
left: 0,
right: 640,
bottom: 0,
toJSON() {},
}));
}
return node;
});
expect(resolveCSSLength(el, 'not-a-length')).toBe(0);
createElementSpy.mockRestore();
});
it('Returns zero when a CSS length resolves to auto', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const getComputedStyleSpy = vi
.spyOn(globalThis, 'getComputedStyle')
.mockImplementation((target: Element) =>
target === el
? ({ length: 0, fontSize: '14px' } as CSSStyleDeclaration)
: ({ inlineSize: 'auto' } as CSSStyleDeclaration)
);
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => ({
x: 0,
y: 0,
width: 640,
height: 0,
top: 0,
left: 0,
right: 640,
bottom: 0,
toJSON() {},
}));
}
return node;
});
expect(resolveCSSLength(el, 'var(--missing-size)')).toBe(0);
createElementSpy.mockRestore();
getComputedStyleSpy.mockRestore();
});
it('Falls back to measurement for calc values', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => ({
x: 0,
y: 0,
width: node.style.inlineSize.startsWith('calc(') ? 8 : 0,
height: 0,
top: 0,
left: 0,
right: 8,
bottom: 0,
toJSON() {},
}));
}
return node;
});
expect(resolveCSSLength(el, 'calc(0.5 * 16px)')).toBe(8);
createElementSpy.mockRestore();
});
it('Copies custom properties from the source element when measuring', () => {
const el = document.createElement('div');
const createElement = document.createElement.bind(document);
const getComputedStyleSpy = vi.spyOn(globalThis, 'getComputedStyle').mockImplementation(
() =>
({
length: 1,
fontSize: '14px',
item(index: number) {
return index === 0 ? '--size' : '';
},
getPropertyValue(name: string) {
return name === '--size' ? '16px' : '';
},
}) as CSSStyleDeclaration
);
const createElementSpy = vi.spyOn(document, 'createElement').mockImplementation((tagName: string) => {
const node = createElement(tagName);
if (tagName === 'div') {
vi.spyOn(node, 'getBoundingClientRect').mockImplementation(() => ({
x: 0,
y: 0,
width: node.style.getPropertyValue('--size') === '16px' ? 8 : 0,
height: 0,
top: 0,
left: 0,
right: 8,
bottom: 0,
toJSON() {},
}));
}
return node;
});
expect(resolveCSSLength(el, 'calc(0.5 * var(--size))')).toBe(8);
createElementSpy.mockRestore();
getComputedStyleSpy.mockRestore();
});
});