style(web): unify panel, group and state controls
Standardize panel actions, group filters, segmented modes, workbench tabs, message filters, identity controls and privacy toggles on the shared button scale. Add cascade-aware tests so stale compact sizes and low-contrast states cannot return.
This commit is contained in:
@@ -28,7 +28,7 @@ function collectRules(css: string, rules: Rule[] = []): Rule[] {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const selector = css.slice(preludeStart, index).trim();
|
||||
const prelude = css.slice(preludeStart, index).trim();
|
||||
let cursor = index + 1;
|
||||
let depth = 1;
|
||||
while (cursor < css.length && depth > 0) {
|
||||
@@ -37,10 +37,13 @@ function collectRules(css: string, rules: Rule[] = []): Rule[] {
|
||||
cursor += 1;
|
||||
}
|
||||
const body = css.slice(index + 1, cursor - 1);
|
||||
if (selector.startsWith('@')) {
|
||||
if (prelude.startsWith('@')) {
|
||||
collectRules(body, rules);
|
||||
} else if (selector) {
|
||||
rules.push({ selector, declarations: parseDeclarations(body) });
|
||||
} else if (prelude) {
|
||||
for (const selector of prelude.split(',')) {
|
||||
const trimmed = selector.replace(/\s+/g, ' ').trim();
|
||||
if (trimmed) rules.push({ selector: trimmed, declarations: parseDeclarations(body) });
|
||||
}
|
||||
}
|
||||
preludeStart = cursor;
|
||||
index = cursor;
|
||||
@@ -75,9 +78,8 @@ async function loadStylesheet() {
|
||||
return value.replace(/var\((--[a-z0-9-]+)\)/gi, (_, name: string) => tokens[name] ?? '');
|
||||
};
|
||||
const exact = (selector: string, property: string) =>
|
||||
rules.filter((rule) => rule.selector.replace(/\s+/g, ' ') === selector).at(-1)?.declarations[
|
||||
property
|
||||
];
|
||||
rules.find((rule) => rule.selector === selector && rule.declarations[property] !== undefined)
|
||||
?.declarations[property];
|
||||
return { rules, tokens, wins, resolve, exact };
|
||||
}
|
||||
|
||||
@@ -155,13 +157,14 @@ describe('button system', () => {
|
||||
expect(tokens['--field-radius']).toBe('10px');
|
||||
expect(tokens['--field-border']).toBe('var(--line-strong)');
|
||||
expect(tokens['--field-focus-border']).toBe('var(--button-focus-ring)');
|
||||
expect(exact('button, input, select, textarea', 'border')).toBe(
|
||||
'1px solid var(--field-border)',
|
||||
);
|
||||
expect(exact('button, input, select, textarea', 'background')).toBe('var(--field-bg)');
|
||||
expect(exact('input:focus, select:focus, textarea:focus', 'border-color')).toBe(
|
||||
'var(--field-focus-border)',
|
||||
);
|
||||
for (const selector of ['button', 'input', 'select', 'textarea']) {
|
||||
expect(exact(selector, 'border')).toBe('1px solid var(--field-border)');
|
||||
expect(exact(selector, 'background')).toBe('var(--field-bg)');
|
||||
}
|
||||
|
||||
for (const selector of ['input:focus', 'select:focus', 'textarea:focus']) {
|
||||
expect(exact(selector, 'border-color')).toBe('var(--field-focus-border)');
|
||||
}
|
||||
});
|
||||
|
||||
it('normalises semantic surfaces in notification, log and message centres', async () => {
|
||||
@@ -252,6 +255,86 @@ describe('button system', () => {
|
||||
expect(stale).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps primitive actions aligned with the standard text-button scale', async () => {
|
||||
const { rules, wins } = await loadStylesheet();
|
||||
const standardControls = [
|
||||
'.instance-context nav a',
|
||||
'.dense-table th button',
|
||||
'.organization-add',
|
||||
'.fleet-card-admin-actions button',
|
||||
'.fleet-messages-device-item',
|
||||
'.maintenance-backup-actions a',
|
||||
];
|
||||
const stale = rules
|
||||
.filter((rule) =>
|
||||
standardControls.some((selector) => rule.selector.replace(/\s+/g, ' ').includes(selector)),
|
||||
)
|
||||
.filter((rule) => {
|
||||
const declarations = Object.values(rule.declarations).join(' ');
|
||||
const sizes = [rule.declarations['min-height'], rule.declarations.height].filter(
|
||||
(value): value is string => value !== undefined,
|
||||
);
|
||||
return (
|
||||
sizes.some((value) =>
|
||||
['1.8rem', '2.1rem', '2.3rem', '2.35rem', '2.4rem'].includes(value),
|
||||
) || declarations.includes('border-radius: 999px')
|
||||
);
|
||||
})
|
||||
.map((rule) => rule.selector);
|
||||
|
||||
expect(stale).toEqual([]);
|
||||
|
||||
for (const selector of [
|
||||
'button.primary-action',
|
||||
'a.primary-action',
|
||||
"[class*='animal-btn-middle']",
|
||||
]) {
|
||||
expect(wins(selector, 'min-height')).toBe('var(--button-height)');
|
||||
expect(wins(selector, 'padding')).toBe('0.45rem 0.9rem');
|
||||
expect(wins(selector, 'font-size')).toBe('0.82rem');
|
||||
}
|
||||
|
||||
for (const selector of ['.organization-add', '.maintenance-backup-actions a']) {
|
||||
expect(wins(selector, 'min-height')).toBe('var(--button-height-compact)');
|
||||
expect(wins(selector, 'padding')).toBe('0.3rem 0.7rem');
|
||||
expect(wins(selector, 'font-size')).toBe('0.76rem');
|
||||
expect(wins(selector, 'border-radius')).toBe('var(--button-radius-compact)');
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps grouped, segmented and workbench controls on the shared scale', async () => {
|
||||
const { exact, wins } = await loadStylesheet();
|
||||
const compactControls = [
|
||||
'.fleet-group-tabs button',
|
||||
'.segmented-control button',
|
||||
'.identity-actions button',
|
||||
'.fleet-notifications-retry-all',
|
||||
'.fleet-messages-filter-chip',
|
||||
'.fleet-messages-delete',
|
||||
'.fleet-switch',
|
||||
];
|
||||
|
||||
for (const selector of compactControls) {
|
||||
expect(exact(selector, 'min-height')).toBe('var(--button-height-compact)');
|
||||
expect(exact(selector, 'padding')).toBe('0.3rem 0.7rem');
|
||||
expect(exact(selector, 'font-size')).toBe('0.76rem');
|
||||
}
|
||||
|
||||
// Small touch surfaces deliberately promote panel controls to the standard height.
|
||||
expect(wins('.identity-actions button', 'min-height')).toBe('var(--button-height)');
|
||||
|
||||
expect(wins('.fleet-group-tabs button', 'border-radius')).toBe('var(--button-radius-compact)');
|
||||
expect(wins('.workbench-tabs button', 'min-height')).toBe('var(--button-height-large)');
|
||||
});
|
||||
|
||||
it('keeps privacy toggles on the shared icon-button target', async () => {
|
||||
const { wins } = await loadStylesheet();
|
||||
|
||||
expect(wins('.fleet-phone-privacy-toggle', 'width')).toBe('var(--icon-button-size)');
|
||||
expect(wins('.fleet-phone-privacy-toggle', 'height')).toBe('var(--icon-button-size)');
|
||||
expect(wins('.fleet-phone-privacy-toggle', 'border-radius')).toBe('var(--button-radius)');
|
||||
});
|
||||
|
||||
it('keeps workbench text and focus indicators on the accessible brand scale', async () => {
|
||||
const { tokens, resolve } = await loadStylesheet();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user