Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions app/src/__tests__/tint-contrast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// @vitest-environment node
import { describe, expect, it } from 'vitest';
import { compileString } from 'sass';
import { VIEW_ACCENTS } from '../view-accents';

// Tinted pills (`m.tint`, data-tone badges, DI kind letters) put `--<tone>-text`
// on a 12 to 16% tint of the tone. The pill can sit on a plain, hovered or
// pressed surface, or on a selected row (`--accent` at 12% or 16%). The text must
// keep 4.5:1 on all of them, in both themes and with every accent.

type Rgb = [number, number, number];
type Tokens = Record<string, string>;

const STYLES = new URL('../styles/', import.meta.url).pathname;
const ACCENTS = ['amber', 'ember', 'gold'];
const TONES = ['ok', 'warn', 'danger', 'accent'];

function theme(accent: string): { dark: Tokens; light: Tokens } {
const { css } = compileString(`@use 'theme' with ($accent: ${accent});`, {
loadPaths: [STYLES],
});
const read = (selector: string): Tokens => {
const start = css.indexOf(`${selector} {`);
expect(start, selector).toBeGreaterThanOrEqual(0);
const body = css.slice(start, css.indexOf('}', start));
const tokens: Tokens = {};
for (const [, name, value] of body.matchAll(/--([\w-]+):\s*(#[0-9a-f]{6})\s*;/gi)) {
tokens[name] = value;
}
return tokens;
};
return { dark: read(':root'), light: read(`:root[data-theme=light]`) };
}

const rgb = (hex: string): Rgb => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) as Rgb;
const channel = (v: number) => {
const c = v / 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
};
const luminance = ([r, g, b]: Rgb) =>
0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
const ratio = (a: Rgb, b: Rgb) => {
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (hi + 0.05) / (lo + 0.05);
};
const mix = (top: Rgb, amount: number, under: Rgb): Rgb =>
top.map((v, i) => v * amount + under[i] * (1 - amount)) as Rgb;

function worst(tokens: Tokens, tone: string) {
const under: [string, Rgb][] = ['bg', 'surface', 'surface-2', 'surface-3'].map((k) => [
k,
rgb(tokens[k]),
]);
for (const amount of [12, 16]) {
under.push([
`a selected row (accent ${amount}%)`,
mix(rgb(tokens['accent']), amount / 100, rgb(tokens['surface'])),
]);
}
let min = { ratio: Infinity, on: '' };
for (const amount of [12, 16]) {
for (const [name, backdrop] of under) {
const tint = mix(rgb(tokens[tone]), amount / 100, backdrop);
const value = ratio(rgb(tokens[`${tone}-text`]), tint);
if (value < min.ratio)
min = { ratio: value, on: `--${tone}-text on ${amount}% tint over ${name}` };
}
}
return min;
}

describe('text on tinted pills', () => {
for (const accent of ACCENTS) {
const themes = theme(accent);
for (const mode of ['dark', 'light'] as const) {
for (const tone of TONES) {
it(`${accent} ${mode}: --${tone}-text keeps 4.5:1 on its tint`, () => {
const tokens = themes[mode];
expect(tokens[`${tone}-text`], `--${tone}-text`).toBeDefined();
const min = worst(tokens, tone);
expect(min.ratio, min.on).toBeGreaterThanOrEqual(4.5);
});
}
}
}

// Brand views replace --accent and --accent-text on the panel root, which also
// changes the selected-row tint under every other pill.
const base = theme('amber');
for (const mode of ['dark', 'light'] as const) {
for (const [view, brand] of Object.entries(VIEW_ACCENTS[mode])) {
for (const tone of TONES) {
it(`${view} view ${mode}: --${tone}-text keeps 4.5:1 on its tint`, () => {
const tokens = { ...base[mode], accent: brand.accent, 'accent-text': brand.text };
const min = worst(tokens, tone);
expect(min.ratio, min.on).toBeGreaterThanOrEqual(4.5);
});
}
}
}
});
26 changes: 4 additions & 22 deletions app/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { angularNativePage, type PlatformPage } from './native-page';
import { initialTab, storeTab, storedTab } from './tab-memory';
import { detectBaseURL } from './base-url';
import { clearHighlightsOnHide } from './rpc';
import { viewAccent } from './view-accents';

const HUB_VIEWS = [
'angular',
Expand All @@ -59,22 +60,6 @@ const VIEW_TITLE: Record<View, string> = {
capacitor: 'Capacitor',
};

const VIEW_ACCENT: Partial<Record<View, string>> = {
ngrx: '#d770e8',
analog: '#ff5470',
'angular-native': '#ff4d6d',
nativescript: '#8196ff',
capacitor: '#53b9ff',
};

const VIEW_ACCENT_LIGHT: Partial<Record<View, string>> = {
ngrx: '#a21caf',
analog: '#be123c',
'angular-native': '#be123c',
nativescript: '#3448c5',
capacitor: '#0369a1',
};

const VIEW_TAB: Partial<Record<View, Tab>> = { ngrx: 'store', analog: 'analog' };
const TAB_VIEW: Partial<Record<Tab, View>> = { store: 'ngrx', analog: 'analog' };
const VIEW_TABS: Partial<Record<View, Tab[]>> = {
Expand Down Expand Up @@ -162,7 +147,8 @@ function readView(): View | null {
@Component({
selector: 'app-root',
host: {
'[style.--accent]': 'viewAccent()',
'[style.--accent]': 'viewAccent()?.accent ?? null',
'[style.--accent-text]': 'viewAccent()?.text ?? null',
'(window:message)': 'inspectFromPanel($event)',
},
imports: [
Expand Down Expand Up @@ -782,11 +768,7 @@ export class App implements OnInit, OnDestroy {
{ id: 'network', label: 'SSR & HTTP' },
];
readonly view = signal<View | null>(readView());
readonly viewAccent = computed(() => {
const view = this.view();
const accents = this.themeService.current() === 'light' ? VIEW_ACCENT_LIGHT : VIEW_ACCENT;
return (view && accents[view]) ?? null;
});
readonly viewAccent = computed(() => viewAccent(this.view(), this.themeService.current()));
readonly title = computed(() => {
const view = this.view();
return view ? VIEW_TITLE[view] : 'Pangular Inspector';
Expand Down
44 changes: 15 additions & 29 deletions app/src/pages/analog-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,9 +1131,11 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
--good: var(--ok);
--bad: var(--danger);
--info: #60a5fa;
--info-text: #bfdbfe;

@include m.light {
--info: #1d4ed8;
--info-text: #1e40af;
}
}
:host {
Expand Down Expand Up @@ -1344,7 +1346,7 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
.count[data-tone='warn'],
[role='tab'][aria-selected='true'] .count[data-tone='warn'] {
background: color-mix(in srgb, var(--warn) 12%, transparent);
color: var(--warn);
color: var(--warn-text);
}

.panel {
Expand Down Expand Up @@ -1685,7 +1687,7 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
.pill.live {
border-color: var(--accent-line);
background: color-mix(in srgb, var(--accent) 12%, var(--surface));
color: var(--accent);
color: var(--accent-text);
}
.pill[data-kind='layout'] {
border-color: #94a3b8;
Expand Down Expand Up @@ -1805,30 +1807,20 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
}
}
[data-tone='good'].pill {
border-color: color-mix(in srgb, var(--ok) 30%, transparent);
background: color-mix(in srgb, var(--ok) 12%, transparent);
color: var(--ok);
@include m.tint(ok);
}
[data-tone='warn'].pill,
[data-tone='warn'].chip {
border-color: color-mix(in srgb, var(--warn) 30%, transparent);
background: color-mix(in srgb, var(--warn) 12%, transparent);
color: var(--warn);
@include m.tint(warn);
}
[data-tone='bad'].pill,
[data-tone='bad'].chip {
border-color: color-mix(in srgb, var(--danger) 30%, transparent);
background: color-mix(in srgb, var(--danger) 12%, transparent);
color: var(--danger);
@include m.tint(danger);
}
[data-tone='info'].pill {
border-color: color-mix(in srgb, var(--info) 30%, transparent);
background: color-mix(in srgb, var(--info) 12%, transparent);
color: #bfdbfe;

@include m.light {
color: var(--info);
}
color: var(--info-text);
}

.status {
Expand All @@ -1838,19 +1830,13 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
font-variant-numeric: tabular-nums;
}
.status[data-status='good'] {
border-color: color-mix(in srgb, var(--ok) 30%, transparent);
background: color-mix(in srgb, var(--ok) 12%, transparent);
color: var(--ok);
@include m.tint(ok);
}
.status[data-status='warn'] {
border-color: color-mix(in srgb, var(--warn) 30%, transparent);
background: color-mix(in srgb, var(--warn) 12%, transparent);
color: var(--warn);
@include m.tint(warn);
}
.status[data-status='bad'] {
border-color: color-mix(in srgb, var(--danger) 30%, transparent);
background: color-mix(in srgb, var(--danger) 12%, transparent);
color: var(--danger);
@include m.tint(danger);
}
.method {
min-width: 52px;
Expand All @@ -1870,7 +1856,7 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
@include m.light {
border-color: color-mix(in srgb, #0369a1 30%, transparent);
background: color-mix(in srgb, #0369a1 8%, transparent);
color: #0369a1;
color: #075985;
}
}
.method[data-method='POST'] {
Expand All @@ -1879,7 +1865,7 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
color: #86efac;

@include m.light {
color: var(--ok);
color: var(--ok-text);
}
}
.method[data-method='PUT'],
Expand All @@ -1889,7 +1875,7 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
color: #fde68a;

@include m.light {
color: var(--warn);
color: var(--warn-text);
}
}
.method[data-method='DELETE'] {
Expand All @@ -1898,7 +1884,7 @@ function walk(routes: AnalogRoute[], depth = 0, out: { route: AnalogRoute; depth
color: #fecaca;

@include m.light {
color: var(--danger);
color: var(--danger-text);
}
}
.request {
Expand Down
14 changes: 7 additions & 7 deletions app/src/pages/component-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,14 +876,14 @@ function bare(name: string): string {
line-height: 18px;
}
.defer-state-complete {
@include m.soft(var(--ok));
@include m.tint(ok);
}
.defer-state-error {
@include m.soft(var(--danger));
@include m.tint(danger);
}
.defer-state-loading,
.defer-state-placeholder {
@include m.soft(var(--warn));
@include m.tint(warn);
}
.triggers {
min-width: 0;
Expand Down Expand Up @@ -1070,7 +1070,7 @@ function bare(name: string): string {
.badge {
padding: 2px 8px;
border-radius: 99px;
@include m.soft(var(--accent));
@include m.tint(accent);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
Expand Down Expand Up @@ -1188,7 +1188,7 @@ function bare(name: string): string {
font-size: 12px;
}
.chip.on {
@include m.soft(var(--ok));
@include m.tint(ok);
}
.chip-note {
color: var(--text-2);
Expand Down Expand Up @@ -1321,14 +1321,14 @@ function bare(name: string): string {
flex: none;
padding: 1px 8px;
border-radius: 99px;
@include m.soft(var(--accent));
@include m.tint(accent);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.kind-flag.directive {
@include m.soft(var(--ok));
@include m.tint(ok);
}
.row-main {
display: flex;
Expand Down
14 changes: 8 additions & 6 deletions app/src/pages/di-inspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ interface InjectorTreeState extends InjectorPage {

const NULL_ID = 'inj-null';

// Text tones: these color the kind letter and badge on a 14 to 16% tint of
// themselves, so they use the stronger `-text` tokens.
const KIND_TONE: Record<string, string> = {
component: 'var(--accent)',
component: 'var(--accent-text)',
directive: 'var(--directive)',
environment: 'var(--ok)',
environment: 'var(--ok-text)',
null: 'var(--text-3)',
};

Expand Down Expand Up @@ -567,7 +569,7 @@ function isTree(value: unknown): value is InjectorNode[] {
font-size: 13px;

@include m.light {
--directive: #1d4ed8;
--directive: #1e40af;
}
}
.mono {
Expand Down Expand Up @@ -945,7 +947,7 @@ function isTree(value: unknown): value is InjectorNode[] {
font-size: 11px;
}
.count.provides {
@include m.soft(var(--ok));
@include m.tint(ok);
}
.count svg {
width: 11px;
Expand Down Expand Up @@ -1123,7 +1125,7 @@ function isTree(value: unknown): value is InjectorNode[] {
line-height: 16px;
}
.kind-flag {
@include m.soft(var(--accent));
@include m.tint(accent);
}
.dep-meta {
display: flex;
Expand Down Expand Up @@ -1307,7 +1309,7 @@ function isTree(value: unknown): value is InjectorNode[] {
font-size: 11px;
padding: 1px 8px;
border-radius: 99px;
@include m.soft(var(--ok));
@include m.tint(ok);
border-width: 1px;
border-style: solid;
}
Expand Down
Loading
Loading