mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
style: spell it "color" everywhere
283 occurrences across 23 files, all of them mine and all of them in the theme/UI work: comments, test names, and a handful of local identifiers (`colours`, `roleColours`, `awaitColoured`) plus one test fixture tag named 'Uncoloured'. Nothing was required to keep the other spelling. Checked before replacing: no CSS custom property or class name contains it, no library prop or API key does, and every string holding it is a test name or our own copy. The renamed identifiers were checked for collisions with an existing `color`/`colors` in the same file first -- there were none. Verified as a pure rename: 262 lines changed, and normalising the spelling on every removed line reproduces the added line exactly, for all 262. So the diff contains no other edit hiding in it. 1134 jest / 64 suites, 271 cypress component tests with the same 12 pre-existing Semantic-era failures in Tags/Channels/Download/Inventory, clean npm run build.
This commit is contained in:
parent
155b490adb
commit
4a4a939f63
23 changed files with 262 additions and 262 deletions
|
|
@ -20,7 +20,7 @@ import '../../src/themes/fonts.css';
|
|||
import '../../src/themes/tokens.css';
|
||||
import '@mantine/core/styles.css';
|
||||
// App.js loads this too. Without it a Notification renders unstyled here, so a toast had no
|
||||
// surface and no title colour of Mantine's -- and its contrast test could not fail.
|
||||
// surface and no title color of Mantine's -- and its contrast test could not fail.
|
||||
import '@mantine/notifications/styles.css';
|
||||
/*
|
||||
* App.js loads this as well. Rules for things outside the component library live here --
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ html .mantine-Notifications-root[data-position^='top'] {
|
|||
|
||||
.wrolpi-navbar-link:hover,
|
||||
.wrolpi-navbar-link-button:hover {
|
||||
/* A translucent black works on every navbar colour, light or dark. */
|
||||
/* A translucent black works on every navbar color, light or dark. */
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ html .mantine-Notifications-root[data-position^='top'] {
|
|||
* - An anchor takes `a {color: var(--blue)}` from tokens.css, which outranks an inherited
|
||||
* value however the bar sets it. The search glyph is an anchor, and so is every status
|
||||
* icon that links to /admin -- those only looked right because they carry an inline
|
||||
* colour of their own, which still wins here and is meant to.
|
||||
* color of their own, which still wins here and is meant to.
|
||||
* - A Mantine ActionIcon paints `--ai-color`, its own themed blue. The hamburger and the
|
||||
* theme toggle are ActionIcons. The variable is no use as a hook: Mantine writes it
|
||||
* INLINE on the element, so a stylesheet declaring it loses every time (the same trap
|
||||
|
|
@ -200,7 +200,7 @@ pre.wrap-text {
|
|||
|
||||
/*
|
||||
* A card's title and its author/channel/domain line are links, but they are the card's
|
||||
* text -- they take the body colour rather than the link colour, so a grid of cards reads
|
||||
* text -- they take the body color rather than the link color, so a grid of cards reads
|
||||
* as titles and not as a wall of blue. This was a literal `black` until the themes
|
||||
* arrived, which left every title at about 1.1:1 on the dark, night and amber panels.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export function useTags() {
|
|||
{...props} // onClick passed here.
|
||||
className={className}
|
||||
/*
|
||||
* The contrasting text colour goes in a custom property, not in `color`. As
|
||||
* The contrasting text color goes in a custom property, not in `color`. As
|
||||
* `color` it was an inline declaration, which no stylesheet rule can outrank --
|
||||
* so night, where a tag becomes an outline over a near-black page, still got
|
||||
* the black text calculated for a bright tag's fill. The tag was legible only
|
||||
|
|
@ -340,7 +340,7 @@ function EditTagsModal() {
|
|||
* `--label-text`, not `color`, for the same reason as the chip itself:
|
||||
* inline `color` cannot be overridden, so in night this preview showed
|
||||
* black text on a transparent outline over a near-black page. It is the
|
||||
* preview of the colour being chosen, so being honest about how the tag
|
||||
* preview of the color being chosen, so being honest about how the tag
|
||||
* will actually look matters here more than anywhere.
|
||||
*/}
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {useTags} from './Tags';
|
|||
* Everything else in the suite takes `NameToTagLabel` from `tagsContextFixture`, which stubs
|
||||
* it as a function returning the name -- so nothing exercised the component that actually
|
||||
* paints a tag. That is how a tag came to be unreadable in night mode without a single test
|
||||
* noticing: the colours are decided here, in the one place in the app that paints with a
|
||||
* noticing: the colors are decided here, in the one place in the app that paints with a
|
||||
* value no theme chose.
|
||||
*/
|
||||
|
||||
|
|
@ -42,12 +42,12 @@ const renderTag = (name, tags) => render(
|
|||
const tagOf = (name) => screen.getByText(name);
|
||||
|
||||
/*
|
||||
* Wait for the fetched colours to arrive. Before they do, `NameToTagLabel` falls back to a
|
||||
* Wait for the fetched colors to arrive. Before they do, `NameToTagLabel` falls back to a
|
||||
* plain `<Label tag>` -- which also carries `wrolpi-tag`, so waiting on the class alone
|
||||
* passes against the fallback and asserts nothing about the real chip. Wait for the user's
|
||||
* colour instead.
|
||||
* color instead.
|
||||
*/
|
||||
const awaitColoured = async (name, color) => {
|
||||
const awaitColored = async (name, color) => {
|
||||
await waitFor(() =>
|
||||
expect(tagOf(name).style.getPropertyValue('--label-color')).toBe(color));
|
||||
return tagOf(name);
|
||||
|
|
@ -59,7 +59,7 @@ describe('a tag chip', () => {
|
|||
tags: [
|
||||
{id: 1, name: 'Water', color: BRIGHT},
|
||||
{id: 2, name: 'Archived', color: DARK},
|
||||
{id: 3, name: 'Uncoloured', color: null},
|
||||
{id: 3, name: 'Uncolored', color: null},
|
||||
],
|
||||
});
|
||||
getRecentTags.mockResolvedValue({tags: []});
|
||||
|
|
@ -68,12 +68,12 @@ describe('a tag chip', () => {
|
|||
it('carries the tag shape, not just the chip shape', async () => {
|
||||
renderTag('Water');
|
||||
|
||||
const tag = await awaitColoured('Water', BRIGHT);
|
||||
const tag = await awaitColored('Water', BRIGHT);
|
||||
expect(tag).toHaveClass('wrolpi-tag');
|
||||
expect(tag).toHaveClass('wrolpi-label');
|
||||
});
|
||||
|
||||
it('puts the calculated text colour in a variable, never in `color`', async () => {
|
||||
it('puts the calculated text color in a variable, never in `color`', async () => {
|
||||
/*
|
||||
* This is the whole defect. As an inline `color` declaration nothing in a stylesheet
|
||||
* could outrank it, so night -- which turns a tag into an outline over a near-black
|
||||
|
|
@ -82,7 +82,7 @@ describe('a tag chip', () => {
|
|||
*/
|
||||
renderTag('Water');
|
||||
|
||||
const tag = await awaitColoured('Water', BRIGHT);
|
||||
const tag = await awaitColored('Water', BRIGHT);
|
||||
expect(tag.style.getPropertyValue('--label-text')).toBe('#000000');
|
||||
expect(tag.style.color).toBe('');
|
||||
});
|
||||
|
|
@ -90,23 +90,23 @@ describe('a tag chip', () => {
|
|||
it('calculates light text for a dark tag', async () => {
|
||||
renderTag('Archived');
|
||||
|
||||
const tag = await awaitColoured('Archived', DARK);
|
||||
const tag = await awaitColored('Archived', DARK);
|
||||
const text = tag.style.getPropertyValue('--label-text');
|
||||
// Non-empty first: an absent variable would satisfy "not black" for free.
|
||||
expect(text).toBeTruthy();
|
||||
expect(text).not.toBe('#000000');
|
||||
});
|
||||
|
||||
it('falls back to a default colour rather than leaving the fill unset', async () => {
|
||||
renderTag('Uncoloured');
|
||||
it('falls back to a default color rather than leaving the fill unset', async () => {
|
||||
renderTag('Uncolored');
|
||||
|
||||
// The tag exists with no colour of its own, so the default stands in.
|
||||
// The tag exists with no color of its own, so the default stands in.
|
||||
await waitFor(() =>
|
||||
expect(tagOf('Uncoloured').style.getPropertyValue('--label-color')).toBeTruthy());
|
||||
expect(tagOf('Uncolored').style.getPropertyValue('--label-color')).toBeTruthy());
|
||||
});
|
||||
|
||||
it('still shows the name before any tags have been fetched', async () => {
|
||||
// The colours are not known yet; the word matters more than the colour.
|
||||
// The colors are not known yet; the word matters more than the color.
|
||||
getTags.mockResolvedValue({tags: []});
|
||||
renderTag('Water');
|
||||
|
||||
|
|
|
|||
|
|
@ -179,8 +179,8 @@ export function isoDatetimeToString(dt, time = false) {
|
|||
* Both card links COMPOSE the caller's className rather than being overwritten by it.
|
||||
* Written as `className='...' {...props}`, the spread put the caller's class last and it
|
||||
* replaced the pair outright -- so every archive title, which passes
|
||||
* `card-title-ellipsis`, lost `card-link` and rendered in the link colour instead of the
|
||||
* card's text colour. The one card type whose classes are set this way, and the one whose
|
||||
* `card-title-ellipsis`, lost `card-link` and rendered in the link color instead of the
|
||||
* card's text color. The one card type whose classes are set this way, and the one whose
|
||||
* title looked wrong.
|
||||
*/
|
||||
const cardLinkClass = (className) =>
|
||||
|
|
@ -1266,7 +1266,7 @@ export function FileIcon({file, disabled = true, size = 48, ...props}) {
|
|||
* The severity of a load average: a warning above half the cores, a problem above three
|
||||
* quarters, and nothing to say below that or when the core count is unknown.
|
||||
*
|
||||
* Exported and pure so the branches can be tested as branches. Reading the colour back off
|
||||
* Exported and pure so the branches can be tested as branches. Reading the color back off
|
||||
* a rendered Statistic cannot do it -- jsdom rejects `color: var(--danger)` as an invalid
|
||||
* declaration and drops it, so every value comes back as the empty string.
|
||||
*/
|
||||
|
|
@ -1577,7 +1577,7 @@ export const toLocaleString = (num, locale = 'en-US') => {
|
|||
}
|
||||
|
||||
/*
|
||||
* The WCAG primitives now live in themes/contrast, so the theme code can measure a colour
|
||||
* The WCAG primitives now live in themes/contrast, so the theme code can measure a color
|
||||
* without importing this module -- Common.js pulls in half the component library, and
|
||||
* `themes/` importing it is how an import cycle starts. Forwarded because a good many call
|
||||
* sites and tests already take `contrastRatio` from here.
|
||||
|
|
@ -1591,8 +1591,8 @@ export const toLocaleString = (num, locale = 'en-US') => {
|
|||
* takes the whole suite with it. A function is read by the spread but not called, so it
|
||||
* dereferences nothing until evaluation has finished.
|
||||
*
|
||||
* The hex parser accepting the `#rgb` shorthand matters and is not incidental: tag colours
|
||||
* are not only chosen in the colour picker, they live in a config file a user edits by hand
|
||||
* The hex parser accepting the `#rgb` shorthand matters and is not incidental: tag colors
|
||||
* are not only chosen in the color picker, they live in a config file a user edits by hand
|
||||
* -- configs are the source of truth in WROLPi. Rejecting `#fff` left the luminance at zero,
|
||||
* so a near-white tag was treated as black and handed light text.
|
||||
*/
|
||||
|
|
@ -1604,7 +1604,7 @@ export const TAG_TEXT_DARK = '#000000';
|
|||
export const TAG_TEXT_LIGHT = '#dddddd';
|
||||
|
||||
/**
|
||||
* Dark or light text, whichever a user's chosen tag colour actually reads better against.
|
||||
* Dark or light text, whichever a user's chosen tag color actually reads better against.
|
||||
*
|
||||
* Measures both instead of comparing a brightness figure to a threshold. The threshold
|
||||
* version put light text on Semantic's blue (`#2185d0`) at 2.9:1 when dark text on the same
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ describe('SearchResultsInput', () => {
|
|||
|
||||
describe('contrastingColor', () => {
|
||||
/*
|
||||
* This decides the text colour on every tag, against a fill the user picked, so it is the
|
||||
* This decides the text color on every tag, against a fill the user picked, so it is the
|
||||
* one place in the app where legibility is computed rather than designed.
|
||||
*/
|
||||
|
||||
|
|
@ -504,27 +504,27 @@ describe('contrastingColor', () => {
|
|||
expect(chosen).toBe(TAG_TEXT_DARK);
|
||||
});
|
||||
|
||||
it('never picks the worse option, for any colour', () => {
|
||||
it('never picks the worse option, for any color', () => {
|
||||
// The property, rather than a list of examples: whatever it returns must be at least as
|
||||
// legible as the alternative would have been.
|
||||
const colours = [
|
||||
const colors = [
|
||||
'#000000', '#ffffff', '#f2f2f2', '#1b1c1d', '#2185d0', '#21ba45', '#db2828',
|
||||
'#fbbd08', '#6435c9', '#a5673f', '#00b5ad', '#e03997', '#b5cc18', '#767676',
|
||||
'#808080', '#7f7f7f',
|
||||
];
|
||||
|
||||
for (const colour of colours) {
|
||||
const chosen = contrastingColor(colour);
|
||||
for (const color of colors) {
|
||||
const chosen = contrastingColor(color);
|
||||
const rejected = chosen === TAG_TEXT_DARK ? TAG_TEXT_LIGHT : TAG_TEXT_DARK;
|
||||
expect(contrastRatio(chosen, colour))
|
||||
.toBeGreaterThanOrEqual(contrastRatio(rejected, colour));
|
||||
expect(contrastRatio(chosen, color))
|
||||
.toBeGreaterThanOrEqual(contrastRatio(rejected, color));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns one of the two text colours and nothing else', () => {
|
||||
it('returns one of the two text colors and nothing else', () => {
|
||||
// It is written into `--label-text`; anything undefined leaves the tag unstyled.
|
||||
for (const colour of ['#000000', '#ffffff', '#2185d0']) {
|
||||
expect([TAG_TEXT_DARK, TAG_TEXT_LIGHT]).toContain(contrastingColor(colour));
|
||||
for (const color of ['#000000', '#ffffff', '#2185d0']) {
|
||||
expect([TAG_TEXT_DARK, TAG_TEXT_LIGHT]).toContain(contrastingColor(color));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -537,13 +537,13 @@ describe('contrastingColor', () => {
|
|||
|
||||
describe('contrastingColor with shorthand hex', () => {
|
||||
/*
|
||||
* Tag colours reach the UI from the database, and a tags config is a file a user can edit
|
||||
* Tag colors reach the UI from the database, and a tags config is a file a user can edit
|
||||
* by hand -- configs are the source of truth in WROLPi -- so a perfectly valid `#fff` can
|
||||
* arrive here. `hexToRGBArray` used to reject anything but six digits, which left the
|
||||
* luminance at zero: a near-white tag was treated as black and given light text.
|
||||
*/
|
||||
|
||||
it('reads a three-digit hex as the colour it is', () => {
|
||||
it('reads a three-digit hex as the color it is', () => {
|
||||
expect(contrastRatio('#000000', '#fff')).toBeCloseTo(21, 1);
|
||||
expect(contrastRatio('#000000', '#ffffff')).toBeCloseTo(contrastRatio('#000000', '#fff'), 5);
|
||||
});
|
||||
|
|
@ -556,7 +556,7 @@ describe('contrastingColor with shorthand hex', () => {
|
|||
expect(contrastingColor('#000')).toBe(TAG_TEXT_LIGHT);
|
||||
});
|
||||
|
||||
it('agrees with the six-digit form of the same colour', () => {
|
||||
it('agrees with the six-digit form of the same color', () => {
|
||||
for (const [short, long] of [['#fff', '#ffffff'], ['#000', '#000000'],
|
||||
['#f00', '#ff0000'], ['#1b2', '#11bb22']]) {
|
||||
expect(contrastingColor(short)).toBe(contrastingColor(long));
|
||||
|
|
@ -569,7 +569,7 @@ describe('loadRole', () => {
|
|||
* The branches, tested as branches. This used to be a source guard scraping the
|
||||
* function body out of Common.js, because the obvious render test cannot work: jsdom
|
||||
* REJECTS `color: var(--danger)` as an invalid declaration and drops it, so every inline
|
||||
* colour reads back as the empty string and the assertions passed for that reason alone.
|
||||
* color reads back as the empty string and the assertions passed for that reason alone.
|
||||
* Pulling the mapping out as a pure function is the honest fix.
|
||||
*
|
||||
* Thresholds are halves and three quarters of the core count, so each is asserted either
|
||||
|
|
@ -599,7 +599,7 @@ describe('loadRole', () => {
|
|||
|
||||
it('never returns a hue', () => {
|
||||
// The regression this exists for: `orange` is byte-identical to `--text` in night,
|
||||
// so a warning load rendered as an ordinary uncoloured number.
|
||||
// so a warning load rendered as an ordinary uncolored number.
|
||||
[loadRole(1, 4), loadRole(2.5, 4), loadRole(4, 4)].filter(Boolean).forEach(role =>
|
||||
expect(role).not.toMatch(/^(red|green|amber|yellow|orange|blue)$/));
|
||||
});
|
||||
|
|
@ -689,7 +689,7 @@ describe('a heading keeps its help icon on the same row as the text', () => {
|
|||
describe('card links', () => {
|
||||
/*
|
||||
* `.card-link` is what makes a card's title read as the card's text rather than as a
|
||||
* link, so a title that misses it is the one thing on the card in the wrong colour.
|
||||
* link, so a title that misses it is the one thing on the card in the wrong color.
|
||||
* Both helpers set the class and then spread the caller's props over it, so any call
|
||||
* site passing a className of its own -- which is every archive title, via
|
||||
* `card-title-ellipsis` -- silently replaced the class instead of adding to it.
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ const NavDropdownTrigger = React.forwardRef(({text, className, ...props}, ref) =
|
|||
* `className` is pulled out and MERGED, never spread over. Menu.Target clones its child
|
||||
* and hands it a className of its own, and a `{...props}` spread placed after the
|
||||
* attribute replaces ours with it -- which left the real More button with no class at
|
||||
* all, so it rendered as a bare grey UA button instead of taking the bar's colour, its
|
||||
* all, so it rendered as a bare grey UA button instead of taking the bar's color, its
|
||||
* padding and its hover. The hidden placeholder is not cloned by Menu.Target, so it
|
||||
* kept its class and measured wider than the button that actually drew.
|
||||
*/
|
||||
|
|
@ -205,7 +205,7 @@ export function NavBar() {
|
|||
const navColor = useNavColorSetting();
|
||||
/*
|
||||
* The bar's foreground is measured against the background the theme resolved for the
|
||||
* user's colour, not taken from a single token. See themes/navColors.
|
||||
* user's color, not taken from a single token. See themes/navColors.
|
||||
*/
|
||||
const navColors = useNavColors(navColor);
|
||||
const wrolpiIcon = <img src='/icon.svg' height='32px' width='32px' alt='WROLPi Home Icon'/>;
|
||||
|
|
@ -241,9 +241,9 @@ export function NavBar() {
|
|||
if (memoryPercent > 80) {
|
||||
const color = memoryPercent > 90 ? highWarningColor : lowWarningColor;
|
||||
{/*
|
||||
The colour goes on the Icon, as it does for every other indicator. It used to
|
||||
The color goes on the Icon, as it does for every other indicator. It used to
|
||||
be passed to `Link`, which ignores it, so the memory warning never actually
|
||||
changed colour -- 81% and 95% looked identical.
|
||||
changed color -- 81% and 95% looked identical.
|
||||
*/}
|
||||
const icon = <Link to='/admin/status'>
|
||||
<Icon name='microchip' size='large' label='Memory usage warning'
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@ const Row = ({children}) => <div style={{display: 'flex', gap: 10, flexWrap: 'wr
|
|||
</div>;
|
||||
|
||||
/*
|
||||
* One navigation bar, in one of the twelve colours a user can pick for it.
|
||||
* One navigation bar, in one of the twelve colors a user can pick for it.
|
||||
*
|
||||
* The bar is the only surface in the app whose background the USER chooses, and every theme
|
||||
* resolves those twelve names to twelve of its own colours -- forty-eight backgrounds that
|
||||
* resolves those twelve names to twelve of its own colors -- forty-eight backgrounds that
|
||||
* the same links and status icons have to stay legible on. They did not: the bar drew every
|
||||
* one of them in `--btn-text`, which is near-black in three of the four themes, and night's
|
||||
* `--brown` is #451212. That glyph was 1.33:1 against the bar behind it.
|
||||
|
|
@ -108,11 +108,11 @@ export const NavBarSample = ({color}) => {
|
|||
<div className='wrolpi-navbar-right' style={{gap: '0.5em', paddingRight: '0.5em'}}>
|
||||
{/*
|
||||
* All three shapes the real bar puts in this corner, because they take
|
||||
* their colour by three different routes and only one of them was ever
|
||||
* their color by three different routes and only one of them was ever
|
||||
* right. A bare Icon inherits. An anchor does NOT -- `a {color:
|
||||
* var(--blue)}` in tokens.css outranks the inherited value, which is what
|
||||
* made the search icon blue on every bar in every theme. And a Mantine
|
||||
* ActionIcon paints its own themed colour, which is what made the
|
||||
* ActionIcon paints its own themed color, which is what made the
|
||||
* hamburger blue. The first version of this sample used an IconButton for
|
||||
* the search icon too, so it showed the second fault twice and hid the
|
||||
* first entirely.
|
||||
|
|
@ -175,12 +175,12 @@ export function ThemeSamplePage() {
|
|||
<Section label='Navigation bars'>
|
||||
<Panel>
|
||||
<p style={{fontSize: 12, color: 'var(--muted)', marginTop: 0}}>
|
||||
Every colour the navigation bar can be set to (Settings → navbar
|
||||
colour), in the <strong>{theme}</strong> theme. The text and icons are not
|
||||
a fixed colour: each bar is measured, and drawn in whichever of this
|
||||
theme's own colours reads best on it. The ratio under each bar is that
|
||||
Every color the navigation bar can be set to (Settings → navbar
|
||||
color), in the <strong>{theme}</strong> theme. The text and icons are not
|
||||
a fixed color: each bar is measured, and drawn in whichever of this
|
||||
theme's own colors reads best on it. The ratio under each bar is that
|
||||
measurement — anything under 4.5:1 is called out, and means the palette
|
||||
itself has no colour that reads on that background.
|
||||
itself has no color that reads on that background.
|
||||
</p>
|
||||
{navColorNames.map(color => <NavBarSample key={color} color={color}/>)}
|
||||
</Panel>
|
||||
|
|
@ -284,11 +284,11 @@ export function ThemeSamplePage() {
|
|||
<Statistic value={0} label='Downloading'/>
|
||||
</StatisticGroup>
|
||||
|
||||
{/* Status colours a reading that carries meaning. Check these in night and amber,
|
||||
{/* Status colors a reading that carries meaning. Check these in night and amber,
|
||||
where there is no hue to spend and the value has to read some other way. */}
|
||||
{/* The group above ends in a label, and a heading has no margin of its own, so
|
||||
without this the two run together. */}
|
||||
<Header as='h5' style={{marginTop: 24}}>Coloured readings, as Status draws them</Header>
|
||||
<Header as='h5' style={{marginTop: 24}}>Colored readings, as Status draws them</Header>
|
||||
<StatisticGroup>
|
||||
<Statistic value='0.4' label='1 Min. Load'/>
|
||||
<Statistic value='2.6' label='5 Min. Load' color='warning'/>
|
||||
|
|
@ -417,7 +417,7 @@ export function ThemeSamplePage() {
|
|||
<p style={{fontSize: 12, color: 'var(--muted)', marginBottom: 0}}>
|
||||
Top right, five at a time, five seconds each unless the caller says
|
||||
otherwise. Check each type in all four themes: night has no second hue to
|
||||
spend, so info, success, warning and error cannot be told apart by colour
|
||||
spend, so info, success, warning and error cannot be told apart by color
|
||||
there.
|
||||
</p>
|
||||
</Panel>
|
||||
|
|
@ -454,7 +454,7 @@ export function ThemeSamplePage() {
|
|||
</div>
|
||||
<p style={{fontSize: 12, color: 'var(--muted)', marginTop: 14, marginBottom: 0}}>
|
||||
Components ask for a <em>meaning</em> — <code>danger</code> — not a hue.
|
||||
Light and dark spend a colour on each. Night and amber have only one hue,
|
||||
Light and dark spend a color on each. Night and amber have only one hue,
|
||||
so a role is a step on a brightness ramp instead; that is why an error and
|
||||
an info toast used to be the same pixel there. Severity climbs in the order
|
||||
shown for those two themes, and both <code>warning</code> and{' '}
|
||||
|
|
@ -658,7 +658,7 @@ export function ThemeSamplePage() {
|
|||
<p style={{fontSize: 12, color: 'var(--muted)', marginTop: 8, marginBottom: 0}}>
|
||||
ColoredInput welds a Label to one edge of a field — the calculators' unit
|
||||
markers. In night and amber the Label is an outline rather than a fill, so
|
||||
the marker reads without a block of colour.
|
||||
the marker reads without a block of color.
|
||||
</p>
|
||||
<div style={{marginTop: 14, display: 'flex', flexDirection: 'column', gap: 10}}>
|
||||
<Checkbox label='Download comments' checked={comments}
|
||||
|
|
@ -735,11 +735,11 @@ export function ThemeSamplePage() {
|
|||
<Section label='Tags'>
|
||||
<Panel>
|
||||
{/*
|
||||
* Tag colours are the user's, stored per tag, so these are raw hex values --
|
||||
* Tag colors are the user's, stored per tag, so these are raw hex values --
|
||||
* the only place in the app that paints with something no theme chose. That
|
||||
* is exactly why they belong here: the gallery had label swatches in theme
|
||||
* colours and no user-coloured tag, so nobody saw that night was calculating
|
||||
* the text colour against a fill it had already thrown away.
|
||||
* colors and no user-colored tag, so nobody saw that night was calculating
|
||||
* the text color against a fill it had already thrown away.
|
||||
*
|
||||
* Deliberately spans the range: near-white and near-black fills, where the
|
||||
* black-or-light text decision flips, plus enough tags to wrap a row.
|
||||
|
|
@ -758,7 +758,7 @@ export function ThemeSamplePage() {
|
|||
</Row>
|
||||
<p style={{marginTop: 14, fontSize: 13, color: 'var(--muted)'}}>
|
||||
Every tag must be readable in all four themes. In night and amber the
|
||||
user's colour is discarded, so all tags look alike — that is the same
|
||||
user's color is discarded, so all tags look alike — that is the same
|
||||
trade the labels above make, and it is deliberate.
|
||||
</p>
|
||||
</Panel>
|
||||
|
|
@ -872,13 +872,13 @@ export function ThemeSamplePage() {
|
|||
/>)}
|
||||
</CardGroup>
|
||||
<p style={{fontSize: 12, color: 'var(--muted)', marginTop: 10}}>
|
||||
Titles are links but take the body colour, not the link colour — a grid of
|
||||
Titles are links but take the body color, not the link color — a grid of
|
||||
results should read as titles. The meta line sits directly under the title;
|
||||
actions are pushed to the foot, so a row lines its buttons up however many
|
||||
lines each title took. A card with no actions simply ends.
|
||||
</p>
|
||||
<p style={{fontSize: 12, color: 'var(--muted)', marginTop: 10}}>
|
||||
The bottom edge carries the file type's colour, so a grid of results is
|
||||
The bottom edge carries the file type's color, so a grid of results is
|
||||
scannable by kind before any title is read. It comes from a token, so night
|
||||
and amber render the accent in their own single hue rather than four.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -177,9 +177,9 @@ export interface ColoredInputProps
|
|||
*/
|
||||
style?: React.CSSProperties;
|
||||
/*
|
||||
* A token colour name for the label, and the SAME union Label accepts rather than a
|
||||
* A token color name for the label, and the SAME union Label accepts rather than a
|
||||
* loose `string`: the point of the union is that a theme must have a token by that
|
||||
* name, and widening it here would let a call site name a colour that resolves to
|
||||
* name, and widening it here would let a call site name a color that resolves to
|
||||
* nothing in every theme.
|
||||
*/
|
||||
color?: LabelProps['color'];
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export interface HeaderProps extends Omit<React.HTMLAttributes<HTMLHeadingElemen
|
|||
/** Hairline rule underneath, separating the section from what follows. */
|
||||
dividing?: boolean;
|
||||
/**
|
||||
* A token colour name (`green`, `red`, ...) for a heading that carries meaning —
|
||||
* A token color name (`green`, `red`, ...) for a heading that carries meaning —
|
||||
* "3 items to add" against "2 items to remove". Call sites kept reaching for an
|
||||
* inline style to do this, which is how a hex ends up in the markup.
|
||||
*/
|
||||
|
|
@ -112,7 +112,7 @@ export interface StatisticProps extends Omit<React.HTMLAttributes<HTMLDivElement
|
|||
/** The number. Rendered as given, so `0` shows as a zero rather than nothing. */
|
||||
value: React.ReactNode;
|
||||
label: React.ReactNode;
|
||||
/** A token colour name, for a reading that carries meaning: load, temperature, IO wait. */
|
||||
/** A token color name, for a reading that carries meaning: load, temperature, IO wait. */
|
||||
color?: string;
|
||||
}
|
||||
|
||||
|
|
@ -164,8 +164,8 @@ export interface CardProps {
|
|||
actions?: React.ReactNode;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
/**
|
||||
* A token colour name, drawn as an accent along the card's top edge. File cards use
|
||||
* it to carry the mimetype's colour, so a grid of results is scannable by kind before
|
||||
* A token color name, drawn as an accent along the card's top edge. File cards use
|
||||
* it to carry the mimetype's color, so a grid of results is scannable by kind before
|
||||
* any text is read. It remaps per theme, so night gets a red accent rather than a
|
||||
* dozen hues.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ describe('toast', () => {
|
|||
.toMatch(/--notification-color:\s*var\(--mantine-color-danger-/);
|
||||
});
|
||||
|
||||
it('gives each type a colour of its own', async () => {
|
||||
it('gives each type a color of its own', async () => {
|
||||
/*
|
||||
* Four types that must not collapse into one in light and dark. Night and amber are
|
||||
* the deliberate exception -- they have no second hue to spend -- but that is decided
|
||||
|
|
@ -143,16 +143,16 @@ describe('toast', () => {
|
|||
*/
|
||||
withToasts();
|
||||
|
||||
const colours = new Set();
|
||||
const colors = new Set();
|
||||
for (const type of ['info', 'success', 'warning', 'error']) {
|
||||
showToast({type, title: `A ${type}`});
|
||||
}
|
||||
await waitFor(() => expect(visibleToasts()).toHaveLength(4));
|
||||
for (const notification of visibleToasts()) {
|
||||
colours.add((notification.getAttribute('style') || '').match(/--notification-color:[^;]*/)?.[0]);
|
||||
colors.add((notification.getAttribute('style') || '').match(/--notification-color:[^;]*/)?.[0]);
|
||||
}
|
||||
|
||||
expect(colours.size).toBe(4);
|
||||
expect(colors.size).toBe(4);
|
||||
});
|
||||
|
||||
it('defaults to info when given no type', async () => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const hexToRgb = (hex) => {
|
|||
};
|
||||
|
||||
/*
|
||||
* Normalise a colour to rgb(), and REFUSE anything that is neither a hex nor already
|
||||
* Normalise a color to rgb(), and REFUSE anything that is neither a hex nor already
|
||||
* rgb(). Strictness is the point: feeding an unresolved `var(--blue)` to a luminance
|
||||
* calculation yields NaN, and `expect(NaN).to.not.equal(x)` passes -- a test that cannot
|
||||
* fail. Better to blow up naming the value than to quietly measure nothing.
|
||||
|
|
@ -38,7 +38,7 @@ const toRgb = (value) => {
|
|||
const trimmed = String(value).trim();
|
||||
if (/^rgba?\(/.test(trimmed)) return trimmed;
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(trimmed)) return hexToRgb(trimmed);
|
||||
throw new Error(`Not a resolved colour: "${trimmed}"`);
|
||||
throw new Error(`Not a resolved color: "${trimmed}"`);
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
@ -173,8 +173,8 @@ describe('ActionInput', () => {
|
|||
});
|
||||
|
||||
/* WCAG contrast between two rgb() strings, for judging how loud a border is. */
|
||||
const luminance = (colour) => {
|
||||
const [r, g, b] = (colour.match(/[\d.]+/g) || []).map(Number);
|
||||
const luminance = (color) => {
|
||||
const [r, g, b] = (color.match(/[\d.]+/g) || []).map(Number);
|
||||
const channel = (v) => {
|
||||
const s = v / 255;
|
||||
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
||||
|
|
@ -216,28 +216,28 @@ describe('semantic roles read as severity in every theme', () => {
|
|||
*/
|
||||
const ROLES = ['neutral', 'info', 'success', 'warning', 'danger'];
|
||||
|
||||
const roleColours = () => {
|
||||
const roleColors = () => {
|
||||
const root = getComputedStyle(document.documentElement);
|
||||
const panel = toRgb(root.getPropertyValue('--panel'));
|
||||
return {
|
||||
panel,
|
||||
text: toRgb(root.getPropertyValue('--text')),
|
||||
roles: ROLES.map(role => ({role, colour: toRgb(root.getPropertyValue(`--${role}`))})),
|
||||
roles: ROLES.map(role => ({role, color: toRgb(root.getPropertyValue(`--${role}`))})),
|
||||
};
|
||||
};
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
it(`gives every role a colour of its own in ${theme}`, () => {
|
||||
it(`gives every role a color of its own in ${theme}`, () => {
|
||||
// True everywhere. Two roles resolving to the same value is the original bug.
|
||||
cy.mountUI(<Panel>roles</Panel>, {theme});
|
||||
|
||||
cy.get('.wrolpi-panel').should(() => {
|
||||
const {roles} = roleColours();
|
||||
const colours = roles.map(r => r.colour);
|
||||
colours.forEach((colour, index) => {
|
||||
expect(colour, `${roles[index].role} resolved`).to.match(/^rgb\(/);
|
||||
const {roles} = roleColors();
|
||||
const colors = roles.map(r => r.color);
|
||||
colors.forEach((color, index) => {
|
||||
expect(color, `${roles[index].role} resolved`).to.match(/^rgb\(/);
|
||||
});
|
||||
expect(new Set(colours).size, `five distinct roles, got ${colours.join(' ')}`)
|
||||
expect(new Set(colors).size, `five distinct roles, got ${colors.join(' ')}`)
|
||||
.to.equal(5);
|
||||
});
|
||||
});
|
||||
|
|
@ -246,24 +246,24 @@ describe('semantic roles read as severity in every theme', () => {
|
|||
cy.mountUI(<Panel>roles</Panel>, {theme});
|
||||
|
||||
cy.get('.wrolpi-panel').should(() => {
|
||||
const {panel, roles} = roleColours();
|
||||
roles.forEach(({role, colour}) => {
|
||||
const {panel, roles} = roleColors();
|
||||
roles.forEach(({role, color}) => {
|
||||
/*
|
||||
* 3:1 is the floor for anything that is not body text. `neutral` is
|
||||
* allowed to sit at the bottom of the ramp -- for `pending` and disabled,
|
||||
* being dim IS the signal -- but it still has to be visible at all.
|
||||
*/
|
||||
expect(contrast(colour, panel), `${role} against the panel`)
|
||||
expect(contrast(color, panel), `${role} against the panel`)
|
||||
.to.be.at.least(role === 'neutral' ? 2.5 : 3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it(`gives each Status kind a colour of its own in ${theme}`, () => {
|
||||
it(`gives each Status kind a color of its own in ${theme}`, () => {
|
||||
/*
|
||||
* The end of the chain, through a real component: four states, four distinct
|
||||
* painted colours. In night these used to need three hand-written
|
||||
* painted colors. In night these used to need three hand-written
|
||||
* `html[data-theme="night"]` overrides; the roles replaced all of them.
|
||||
*/
|
||||
cy.mountUI(
|
||||
|
|
@ -278,8 +278,8 @@ describe('semantic roles read as severity in every theme', () => {
|
|||
|
||||
cy.get('.wrolpi-status').should(($all) => {
|
||||
expect($all.length).to.equal(4);
|
||||
const colours = [...$all].map(el => getComputedStyle(el).color);
|
||||
expect(new Set(colours).size, `four distinct colours, got ${colours.join(' ')}`)
|
||||
const colors = [...$all].map(el => getComputedStyle(el).color);
|
||||
expect(new Set(colors).size, `four distinct colors, got ${colors.join(' ')}`)
|
||||
.to.equal(4);
|
||||
});
|
||||
});
|
||||
|
|
@ -296,8 +296,8 @@ describe('semantic roles read as severity in every theme', () => {
|
|||
cy.mountUI(<Panel>roles</Panel>, {theme});
|
||||
|
||||
cy.get('.wrolpi-panel').should(() => {
|
||||
const {panel, roles} = roleColours();
|
||||
const ratios = roles.map(({role, colour}) => ({role, ratio: contrast(colour, panel)}));
|
||||
const {panel, roles} = roleColors();
|
||||
const ratios = roles.map(({role, color}) => ({role, ratio: contrast(color, panel)}));
|
||||
ratios.slice(1).forEach((entry, index) => {
|
||||
expect(entry.ratio, `${entry.role} is louder than ${ratios[index].role}`)
|
||||
.to.be.greaterThan(ratios[index].ratio);
|
||||
|
|
@ -307,21 +307,21 @@ describe('semantic roles read as severity in every theme', () => {
|
|||
|
||||
it(`makes warning and danger louder than ordinary text in ${theme}`, () => {
|
||||
/*
|
||||
* Only here. A light theme's text is near-black at ~14:1 and no warning colour
|
||||
* Only here. A light theme's text is near-black at ~14:1 and no warning color
|
||||
* will ever match it -- there, hue is what marks a reading as flagged. On a
|
||||
* single hue the roles and the text come off the same ramp, so anything meaning
|
||||
* "look at this" has to outrank the prose or it does not read as flagged at all.
|
||||
*
|
||||
* This is the assertion the Status page needed: an uncoloured load reading
|
||||
* This is the assertion the Status page needed: an uncolored load reading
|
||||
* inherits `--text`, and warning used to sit just below it.
|
||||
*/
|
||||
cy.mountUI(<Panel>roles</Panel>, {theme});
|
||||
|
||||
cy.get('.wrolpi-panel').should(() => {
|
||||
const {panel, text, roles} = roleColours();
|
||||
const {panel, text, roles} = roleColors();
|
||||
const prose = contrast(text, panel);
|
||||
['warning', 'danger'].forEach((name) => {
|
||||
const role = roles.find(r => r.role === name).colour;
|
||||
const role = roles.find(r => r.role === name).color;
|
||||
expect(contrast(role, panel), `${name} against --text on the panel`)
|
||||
.to.be.greaterThan(prose);
|
||||
});
|
||||
|
|
@ -329,7 +329,7 @@ describe('semantic roles read as severity in every theme', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not leave failure to colour alone', () => {
|
||||
it('does not leave failure to color alone', () => {
|
||||
// Brightness is the first thing lost on a dim screen, and it is all night has.
|
||||
cy.mountUI(
|
||||
<Panel>
|
||||
|
|
@ -356,10 +356,10 @@ describe('a load reading ranks itself in the monochrome themes', () => {
|
|||
*
|
||||
* This is the Status page's CPU load, and the clearest payoff of the roles outside the
|
||||
* library: `orange` above half the cores was byte-identical to `--text` in night, so a
|
||||
* machine under load rendered its warning as an ordinary uncoloured number.
|
||||
* machine under load rendered its warning as an ordinary uncolored number.
|
||||
*
|
||||
* Only a browser can see it. jsdom rejects `color: var(--warning)` as invalid and drops
|
||||
* it, so the inline colour reads back empty whatever the component did.
|
||||
* it, so the inline color reads back empty whatever the component did.
|
||||
*/
|
||||
monochromeThemes.forEach((theme) => {
|
||||
it(`separates fine, warning and danger loads in ${theme}`, () => {
|
||||
|
|
@ -615,7 +615,7 @@ describe('a progress bar has room for the label inside it', () => {
|
|||
* The label spans the whole bar, so it sits on the fill AND on the track. This
|
||||
* asserts the track half only, and that is deliberate: over the FILL it measures
|
||||
* 5.49 in light but 2.35 / 2.48 / 2.74 in dark, night and amber -- below any
|
||||
* legibility floor. Fixing that needs a per-half text colour, which is a design
|
||||
* legibility floor. Fixing that needs a per-half text color, which is a design
|
||||
* decision rather than a bug fix, so it is reported rather than quietly asserted
|
||||
* at a threshold low enough to pass. See the note in ui.css.
|
||||
*/
|
||||
|
|
@ -696,7 +696,7 @@ describe('a table header is a surface, not just bold text', () => {
|
|||
* Mantine gives thead no background, so the header painted whatever was behind
|
||||
* the table -- which is exactly what an unstriped body row paints. Against a
|
||||
* striped body the header read as one more stripe, and weight was the only
|
||||
* thing telling them apart. None of this is visible to jsdom: the colours are
|
||||
* thing telling them apart. None of this is visible to jsdom: the colors are
|
||||
* tokens, and "what is behind an unstriped row" is a question about painting.
|
||||
*/
|
||||
cy.mountUI(sampleTable, {theme});
|
||||
|
|
@ -713,7 +713,7 @@ describe('a table header is a surface, not just bold text', () => {
|
|||
* An unstriped row paints nothing, so what shows through is the page.
|
||||
* That is `--bg`, taken from the token -- NOT
|
||||
* getComputedStyle(documentElement).backgroundColor, which is
|
||||
* `rgba(0, 0, 0, 0)` because `body` carries the page colour and the root
|
||||
* `rgba(0, 0, 0, 0)` because `body` carries the page color and the root
|
||||
* element carries none. Comparing an opaque header against that string
|
||||
* can never fail, which is how this passed before it was fixed.
|
||||
*/
|
||||
|
|
@ -884,7 +884,7 @@ describe('a file row lines its icon up with its name', () => {
|
|||
describe('StatisticGroup separates its statistics with nothing but space', () => {
|
||||
/*
|
||||
* The group has no frame, no rules between cells and no surface of its own: the
|
||||
* statistics sit on whatever they were dropped onto and take its colour. That leaves
|
||||
* statistics sit on whatever they were dropped onto and take its color. That leaves
|
||||
* the gap as the only thing keeping them apart, and `auto-fit` decides how many rows
|
||||
* there are at paint time -- so whether they actually stay apart is a question only a
|
||||
* browser can answer. jsdom has no tracks, no rows and no widths.
|
||||
|
|
@ -966,11 +966,11 @@ describe('StatisticGroup separates its statistics with nothing but space', () =>
|
|||
});
|
||||
});
|
||||
|
||||
it('takes no colour of its own, so it sits on the surface it is given', () => {
|
||||
it('takes no color of its own, so it sits on the surface it is given', () => {
|
||||
/*
|
||||
* This is the point of having no chrome: dropped on the page it reads as page, dropped
|
||||
* in a Panel it reads as panel. A background on the group or a cell -- which is how
|
||||
* this was built first, to carry hairlines -- would show as a patch of the wrong colour
|
||||
* this was built first, to carry hairlines -- would show as a patch of the wrong color
|
||||
* on every surface but the one it was picked for.
|
||||
*/
|
||||
cy.mountUI(<div style={{background: 'rgb(1, 2, 3)', padding: 10}}>{sixStatistics}</div>);
|
||||
|
|
@ -1049,7 +1049,7 @@ describe('StatisticGroup separates its statistics with nothing but space', () =>
|
|||
themeNames.forEach((theme) => {
|
||||
it(`reads its value and label from ${theme}'s tokens`, () => {
|
||||
// The contrast between value and label is all that structures a statistic now that
|
||||
// there is no cell around it, so both colours have to resolve in every theme.
|
||||
// there is no cell around it, so both colors have to resolve in every theme.
|
||||
cy.mountUI(inNarrowColumn(sixStatistics), {theme});
|
||||
|
||||
cy.get('.wrolpi-statistic-value').first().then(($value) => {
|
||||
|
|
@ -1057,8 +1057,8 @@ describe('StatisticGroup separates its statistics with nothing but space', () =>
|
|||
|
||||
cy.get('.wrolpi-statistic-label').first().should(($label) => {
|
||||
const label = getComputedStyle($label[0]).color;
|
||||
expect(value, 'value colour resolved').to.match(/^rgba?\(/);
|
||||
expect(label, 'label colour resolved').to.match(/^rgba?\(/);
|
||||
expect(value, 'value color resolved').to.match(/^rgba?\(/);
|
||||
expect(label, 'label color resolved').to.match(/^rgba?\(/);
|
||||
// The label is the muted token and the value is body text; if either fell
|
||||
// back the two would come out the same and the statistic would flatten.
|
||||
expect(label, 'label is distinct from the value').to.not.equal(value);
|
||||
|
|
@ -1068,24 +1068,24 @@ describe('StatisticGroup separates its statistics with nothing but space', () =>
|
|||
});
|
||||
});
|
||||
|
||||
describe('a tag is legible in every theme, whatever colour the user picked', () => {
|
||||
describe('a tag is legible in every theme, whatever color the user picked', () => {
|
||||
/*
|
||||
* Tag colours are the user's, stored per tag -- the only thing in the app painted with a
|
||||
* Tag colors are the user's, stored per tag -- the only thing in the app painted with a
|
||||
* value no theme chose. Tags.js calculates black or light text against that fill, and
|
||||
* night discards the fill entirely (a filled tag would be a bright patch) while amber
|
||||
* replaces it with its own. Whether the text still reads afterwards is a question about
|
||||
* resolved colour against a real painted surface, so it can only be asked here.
|
||||
* resolved color against a real painted surface, so it can only be asked here.
|
||||
*/
|
||||
|
||||
// The extremes of the black-or-light decision, plus a mid tone.
|
||||
const TAG_COLOURS = [
|
||||
const TAG_COLORS = [
|
||||
['Reference', '#f2f2f2'], // near-white: text is calculated black
|
||||
['Archived', '#1b1c1d'], // near-black: text is calculated light
|
||||
['Water', '#2185d0'],
|
||||
];
|
||||
|
||||
const tags = <div>
|
||||
{TAG_COLOURS.map(([name, color]) => <span
|
||||
{TAG_COLORS.map(([name, color]) => <span
|
||||
key={name}
|
||||
className='wrolpi-label wrolpi-tag'
|
||||
data-tag={name}
|
||||
|
|
@ -1094,7 +1094,7 @@ describe('a tag is legible in every theme, whatever colour the user picked', ()
|
|||
</div>;
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
TAG_COLOURS.forEach(([name]) => {
|
||||
TAG_COLORS.forEach(([name]) => {
|
||||
it(`reads ${name} against its real surface in ${theme}`, () => {
|
||||
cy.mountUI(tags, {theme});
|
||||
|
||||
|
|
@ -1111,7 +1111,7 @@ describe('a tag is legible in every theme, whatever colour the user picked', ()
|
|||
|
||||
it('detects the unreadable tag this started as, so the check has teeth', () => {
|
||||
/*
|
||||
* The original defect, reproduced deliberately: the calculated text colour written to
|
||||
* The original defect, reproduced deliberately: the calculated text color written to
|
||||
* `color` inline, which no stylesheet rule can outrank. In night the tag becomes a
|
||||
* transparent outline over a near-black page, so the black text calculated for a
|
||||
* near-white fill is painted onto near-black.
|
||||
|
|
@ -1295,7 +1295,7 @@ describe('a toast is readable in every theme', () => {
|
|||
* near-black in dark, night and amber. The title measured about 1.1:1 against the toast's
|
||||
* own surface: present, and invisible.
|
||||
*
|
||||
* Only a browser can catch that. jsdom resolves no `var()`, so the title's colour there is
|
||||
* Only a browser can catch that. jsdom resolves no `var()`, so the title's color there is
|
||||
* the literal string `var(--mantine-color-white)` and every contrast check passes.
|
||||
*/
|
||||
|
||||
|
|
@ -1332,7 +1332,7 @@ describe('a toast is readable in every theme', () => {
|
|||
/*
|
||||
* The end of the chain, measured rather than inferred. The jest tests assert the
|
||||
* Mantine variable NAME on the style attribute, which would still pass if someone
|
||||
* put four hue names back -- four distinct strings that resolve to two colours here.
|
||||
* put four hue names back -- four distinct strings that resolve to two colors here.
|
||||
* This reads what is painted.
|
||||
*/
|
||||
cy.mountUI(<>
|
||||
|
|
@ -1353,21 +1353,21 @@ describe('a toast is readable in every theme', () => {
|
|||
|
||||
cy.get('[class*="mantine-Notification-root"]').should('have.length', 4);
|
||||
cy.get('[class*="mantine-Notification-root"]').should(($toasts) => {
|
||||
const colours = [...$toasts].map(t =>
|
||||
const colors = [...$toasts].map(t =>
|
||||
getComputedStyle(t).getPropertyValue('--notification-color').trim());
|
||||
expect(new Set(colours).size, `four distinct kinds, got ${colours.join(' ')}`)
|
||||
expect(new Set(colors).size, `four distinct kinds, got ${colors.join(' ')}`)
|
||||
.to.equal(4);
|
||||
|
||||
/*
|
||||
* Distinctness alone does not catch the regression this is for. Reverting to
|
||||
* hue names gives four distinct values in night too -- blue, green, yellow and
|
||||
* red are not the same colour there. What they are is cramped and badly
|
||||
* red are not the same color there. What they are is cramped and badly
|
||||
* ranked, with `--blue` at 2.29:1 against the toast surface: present, and
|
||||
* invisible. So the floor is the assertion that has teeth.
|
||||
*/
|
||||
const surface = toRgb(getComputedStyle($toasts[0]).backgroundColor);
|
||||
colours.forEach((colour, index) => {
|
||||
expect(contrast(toRgb(colour), surface), `toast ${index} against its surface`)
|
||||
colors.forEach((color, index) => {
|
||||
expect(contrast(toRgb(color), surface), `toast ${index} against its surface`)
|
||||
.to.be.at.least(3);
|
||||
});
|
||||
});
|
||||
|
|
@ -1386,9 +1386,9 @@ describe('a toast is readable in every theme', () => {
|
|||
|
||||
cy.get('.wrolpi-message').should(($all) => {
|
||||
expect($all.length).to.equal(4);
|
||||
const colours = [...$all].map(m =>
|
||||
const colors = [...$all].map(m =>
|
||||
getComputedStyle(m).getPropertyValue('--message-color').trim());
|
||||
expect(new Set(colours).size, `four distinct kinds, got ${colours.join(' ')}`)
|
||||
expect(new Set(colors).size, `four distinct kinds, got ${colors.join(' ')}`)
|
||||
.to.equal(4);
|
||||
});
|
||||
});
|
||||
|
|
@ -1397,7 +1397,7 @@ describe('a toast is readable in every theme', () => {
|
|||
it('keeps the title more prominent than the message', () => {
|
||||
/*
|
||||
* Night and amber drop the description's muting, because `--muted` on a panel is 2.0:1
|
||||
* in night. Weight has to carry the hierarchy once colour cannot.
|
||||
* in night. Weight has to carry the hierarchy once color cannot.
|
||||
*/
|
||||
showToast('night');
|
||||
|
||||
|
|
@ -1423,7 +1423,7 @@ describe('a toast is readable in every theme', () => {
|
|||
});
|
||||
|
||||
/*
|
||||
* The colour a box actually shows, looking through the layers in the order given.
|
||||
* The color a box actually shows, looking through the layers in the order given.
|
||||
*
|
||||
* The order is the caller's business, because "the background" is ambiguous once
|
||||
* pseudo-elements are involved and picking wrong makes a test vacuous rather than wrong.
|
||||
|
|
@ -1436,9 +1436,9 @@ describe('a toast is readable in every theme', () => {
|
|||
*/
|
||||
const paintedBackground = (el, order = [null, '::before', '::after']) => {
|
||||
for (const pseudo of order) {
|
||||
const colour = getComputedStyle(el, pseudo).backgroundColor;
|
||||
if (colour && !/^rgba\(.*,\s*0(\.0+)?\)$/.test(colour) && colour !== 'transparent') {
|
||||
return colour;
|
||||
const color = getComputedStyle(el, pseudo).backgroundColor;
|
||||
if (color && !/^rgba\(.*,\s*0(\.0+)?\)$/.test(color) && color !== 'transparent') {
|
||||
return color;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
@ -1449,7 +1449,7 @@ describe('a loading placeholder is visible on the surface it covers', () => {
|
|||
it(`separates the skeleton from the panel in ${theme}`, () => {
|
||||
/*
|
||||
* `--head` and `--border` are what the bars come out as, and neither was chosen
|
||||
* with a panel behind it in mind. If they land on the panel's own colour there
|
||||
* with a panel behind it in mind. If they land on the panel's own color there
|
||||
* is no placeholder at all, only an empty box, and jsdom cannot see that.
|
||||
*/
|
||||
cy.mountUI(<Panel><Placeholder lines={3}/></Panel>, {theme});
|
||||
|
|
@ -1479,7 +1479,7 @@ describe('a loading placeholder is visible on the surface it covers', () => {
|
|||
|
||||
describe('a spinner is visible in every theme', () => {
|
||||
themeNames.forEach((theme) => {
|
||||
it(`resolves the loader colour in ${theme}`, () => {
|
||||
it(`resolves the loader color in ${theme}`, () => {
|
||||
// `color='var(--blue)'` reaches Mantine as a string it does not understand and
|
||||
// passes straight through to CSS. If the token were misspelled the property
|
||||
// would resolve to nothing and the spinner would paint in currentColor -- or,
|
||||
|
|
@ -1557,7 +1557,7 @@ describe('an icon stack carries the surface it was placed on', () => {
|
|||
|
||||
it('takes the surface from an ancestor, which is how the nav bar sets it', () => {
|
||||
/*
|
||||
* The nav bar's colour is a user setting written inline on the <nav>, so the stack
|
||||
* The nav bar's color is a user setting written inline on the <nav>, so the stack
|
||||
* inside it cannot name a token. It sets --icon-stack-bg on the bar itself and lets
|
||||
* it inherit -- and this is the test for that, because setting the property on the
|
||||
* stack (as the file browser does) would pass even if inheritance were broken.
|
||||
|
|
@ -1627,11 +1627,11 @@ describe('theme tokens actually resolve', () => {
|
|||
* panel. jsdom cannot catch that; it does not resolve var() at all.
|
||||
*/
|
||||
cy.get('.wrolpi-path-input-control input').should(($input) => {
|
||||
const colour = getComputedStyle($input[0]).color;
|
||||
expect(colour, 'input text colour resolved').to.match(/^rgba?\(/);
|
||||
const color = getComputedStyle($input[0]).color;
|
||||
expect(color, 'input text color resolved').to.match(/^rgba?\(/);
|
||||
// Only an rgba() alpha of zero is invisible. Matching a trailing ", 0)"
|
||||
// loosely would flag amber's opaque rgb(255, 149, 0) for its blue channel.
|
||||
expect(colour, 'input text is not transparent')
|
||||
expect(color, 'input text is not transparent')
|
||||
.not.to.match(/^rgba\(.*,\s*0(\.0+)?\)$/);
|
||||
});
|
||||
|
||||
|
|
@ -2080,10 +2080,10 @@ describe('pagination sits in the middle of its container', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('the navigation bar is legible on every colour a user can pick', () => {
|
||||
describe('the navigation bar is legible on every color a user can pick', () => {
|
||||
/*
|
||||
* The bar's background is the one surface the USER chooses, by hue name, and each theme
|
||||
* resolves those twelve names to twelve of its own colours. Forty-eight backgrounds; the
|
||||
* resolves those twelve names to twelve of its own colors. Forty-eight backgrounds; the
|
||||
* links and status icons have to read on all of them. They did not -- every one was
|
||||
* drawn in `--btn-text`, near-black in three of the four themes, against backgrounds like
|
||||
* night's `--brown` (#451212). That glyph was 1.33:1 against the bar behind it.
|
||||
|
|
@ -2096,7 +2096,7 @@ describe('the navigation bar is legible on every colour a user can pick', () =>
|
|||
*
|
||||
* `NavBarSample` is the gallery's bar rather than <NavBar/> itself, which needs the
|
||||
* settings, status and worker contexts and a dozen polling hooks. Both build their style
|
||||
* with the same `navBarStyle(useNavColors(colour))` call, and navColors.test.js has a
|
||||
* with the same `navBarStyle(useNavColors(color))` call, and navColors.test.js has a
|
||||
* source guard holding NavBar to that.
|
||||
*/
|
||||
const barsFor = (theme) => {
|
||||
|
|
@ -2106,14 +2106,14 @@ describe('the navigation bar is legible on every colour a user can pick', () =>
|
|||
};
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
it(`paints a resolved background for all twelve colours in ${theme}`, () => {
|
||||
it(`paints a resolved background for all twelve colors in ${theme}`, () => {
|
||||
barsFor(theme);
|
||||
|
||||
navColorNames.forEach((color) => {
|
||||
cy.get(`[data-nav-sample="${color}"] .wrolpi-navbar`).should(($nav) => {
|
||||
const background = getComputedStyle($nav[0]).backgroundColor;
|
||||
// An unresolved token paints nothing at all, and a transparent bar over
|
||||
// the page would read as "the colour just looks wrong" rather than as a
|
||||
// the page would read as "the color just looks wrong" rather than as a
|
||||
// broken style.
|
||||
expect(background, `${theme}/${color} bar background`)
|
||||
.to.not.equal('rgba(0, 0, 0, 0)');
|
||||
|
|
@ -2121,7 +2121,7 @@ describe('the navigation bar is legible on every colour a user can pick', () =>
|
|||
});
|
||||
});
|
||||
|
||||
it(`clears 3:1 on every colour in ${theme}`, () => {
|
||||
it(`clears 3:1 on every color in ${theme}`, () => {
|
||||
/*
|
||||
* 3:1 is the WCAG floor for graphical objects, which is what these icons are.
|
||||
* Twelve of the forty-eight combinations were below it before measurement.
|
||||
|
|
@ -2149,7 +2149,7 @@ describe('the navigation bar is legible on every colour a user can pick', () =>
|
|||
* They inherit nothing. An anchor takes `a {color: var(--blue)}` from
|
||||
* tokens.css, which outranks the bar's inherited value; a Mantine ActionIcon
|
||||
* paints `--ai-color`, its own themed blue, and Mantine writes that variable
|
||||
* INLINE so no stylesheet variable can reach it. Three routes to a colour, of
|
||||
* INLINE so no stylesheet variable can reach it. Three routes to a color, of
|
||||
* which one worked.
|
||||
*
|
||||
* `querySelectorAll`, and the count asserted, because the version of this test
|
||||
|
|
@ -2240,7 +2240,7 @@ describe('the navigation bar is legible on every colour a user can pick', () =>
|
|||
|
||||
describe('the navigation bar corner', () => {
|
||||
/*
|
||||
* The real <DesktopNav/>, which takes its colours, home link and indicators as props and
|
||||
* The real <DesktopNav/>, which takes its colors, home link and indicators as props and
|
||||
* so needs none of the polling hooks <NavBar/> wires up. The indicators are the shapes
|
||||
* the app actually puts there, in the real NavIconWrapper: a bare-icon anchor (share), an
|
||||
* unwrapped anchor (search), and two IconButtons (hotspot, theme picker). Those three
|
||||
|
|
@ -2428,7 +2428,7 @@ describe('the navbar overflow menu keeps the bar\'s styling', () => {
|
|||
* trigger spread those cloned props AFTER its own `className` attribute, so the cloned
|
||||
* value replaced ours and the real More button rendered with NO class at all -- a bare
|
||||
* UA button, grey on dark and white on light, while every other item in the bar took the
|
||||
* user's navbar colour.
|
||||
* user's navbar color.
|
||||
*
|
||||
* The hidden placeholder DesktopNav measures is NOT cloned by Menu.Target, so it kept its
|
||||
* class and stayed the width the styled button would have been. That is why the width
|
||||
|
|
@ -2454,7 +2454,7 @@ describe('the navbar overflow menu keeps the bar\'s styling', () => {
|
|||
const moreButton = () => cy.contains('.wrolpi-navbar button', 'More');
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
it(`draws More in the bar's own colour in ${theme}`, () => {
|
||||
it(`draws More in the bar's own color in ${theme}`, () => {
|
||||
mountNarrow(theme);
|
||||
|
||||
cy.get('.wrolpi-navbar').then(($nav) => {
|
||||
|
|
@ -2463,7 +2463,7 @@ describe('the navbar overflow menu keeps the bar\'s styling', () => {
|
|||
const style = getComputedStyle($more[0]);
|
||||
expect($more[0].className, 'More carries the bar classes')
|
||||
.to.contain('wrolpi-navbar-link');
|
||||
expect(style.color, `More text colour in ${theme}`).to.equal(expected);
|
||||
expect(style.color, `More text color in ${theme}`).to.equal(expected);
|
||||
// A UA button paints a grey face; the bar's items are transparent.
|
||||
expect(style.backgroundColor, `More background in ${theme}`)
|
||||
.to.equal('rgba(0, 0, 0, 0)');
|
||||
|
|
@ -2842,22 +2842,22 @@ describe('a toast does not cover the navigation bar', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('a disabled button keeps its own colour', () => {
|
||||
describe('a disabled button keeps its own color', () => {
|
||||
/*
|
||||
* Mantine repaints a disabled button flat grey -- background, text and border all
|
||||
* replaced with `--mantine-color-disabled`. Semantic kept the colour and dropped the
|
||||
* replaced with `--mantine-color-disabled`. Semantic kept the color and dropped the
|
||||
* whole control to `opacity: 0.45`, so a disabled Delete was still visibly the red one.
|
||||
*
|
||||
* The file browser is where this bites. Its footer is eight buttons, six of which are
|
||||
* disabled until something is selected, so the toolbar a user meets on arriving at /files
|
||||
* is an undifferentiated grey row -- Delete, Rename, Move, Ignore and Tag all identical.
|
||||
* Colour is how those are told apart at a glance, and disabling them threw it away.
|
||||
* Color is how those are told apart at a glance, and disabling them threw it away.
|
||||
*
|
||||
* Opacity is the better signal anyway: it says "not available" without also saying "no
|
||||
* longer the delete button".
|
||||
*
|
||||
* Mantine leaves `--button-bg` intact on a disabled button -- it overrides the painted
|
||||
* `background` rather than the variable -- so the colour the call site asked for is still
|
||||
* `background` rather than the variable -- so the color the call site asked for is still
|
||||
* there to be read back.
|
||||
*/
|
||||
const pairs = [
|
||||
|
|
@ -2876,11 +2876,11 @@ describe('a disabled button keeps its own colour', () => {
|
|||
</div>, {theme});
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
it(`keeps every colour recognisable while disabled in ${theme}`, () => {
|
||||
it(`keeps every color recognisable while disabled in ${theme}`, () => {
|
||||
mountPairs(theme);
|
||||
|
||||
cy.get('[data-testid^="off-"]').should(($disabled) => {
|
||||
expect($disabled, 'a disabled button per colour').to.have.length(pairs.length);
|
||||
expect($disabled, 'a disabled button per color').to.have.length(pairs.length);
|
||||
|
||||
pairs.forEach(({color}) => {
|
||||
const on = Cypress.$(`[data-testid="on-${color}"]`)[0];
|
||||
|
|
@ -2894,7 +2894,7 @@ describe('a disabled button keeps its own colour', () => {
|
|||
|
||||
it(`still marks them as unavailable in ${theme}`, () => {
|
||||
/*
|
||||
* Keeping the colour must not cost the affordance: a disabled button that looks
|
||||
* Keeping the color must not cost the affordance: a disabled button that looks
|
||||
* identical to an enabled one is a worse bug than a grey one.
|
||||
*
|
||||
* Against `--disabled-opacity` rather than a band. A band tolerates an
|
||||
|
|
@ -2918,7 +2918,7 @@ describe('a disabled button keeps its own colour', () => {
|
|||
});
|
||||
|
||||
themeNames.forEach((theme) => {
|
||||
it(`tells two disabled buttons of different colours apart in ${theme}`, () => {
|
||||
it(`tells two disabled buttons of different colors apart in ${theme}`, () => {
|
||||
/*
|
||||
* The claim stated as the user meets it, and the inverse of the bug: five grey
|
||||
* blobs were five EQUAL greys. Comparing each against its enabled twin above
|
||||
|
|
@ -2935,7 +2935,7 @@ describe('a disabled button keeps its own colour', () => {
|
|||
.map(button => getComputedStyle(button).backgroundColor);
|
||||
|
||||
expect(new Set(fills).size,
|
||||
`${theme}: five colours, distinct fills: ${fills.join(' ')}`)
|
||||
`${theme}: five colors, distinct fills: ${fills.join(' ')}`)
|
||||
.to.equal(pairs.length);
|
||||
});
|
||||
});
|
||||
|
|
@ -2984,7 +2984,7 @@ describe('a disabled button keeps its own colour', () => {
|
|||
|
||||
expect(style.backgroundColor, 'no fill').to.equal('rgba(0, 0, 0, 0)');
|
||||
expect(style.borderStyle, 'still dashed').to.equal('dashed');
|
||||
expect(toRgb(style.borderTopColor), 'in the danger colour')
|
||||
expect(toRgb(style.borderTopColor), 'in the danger color')
|
||||
.to.equal(toRgb(style.getPropertyValue('--danger') || '#ff5757'));
|
||||
expect(parseFloat(style.opacity), 'and still faded').to.be.lessThan(1);
|
||||
});
|
||||
|
|
@ -3018,15 +3018,15 @@ describe('a disabled button keeps its own colour', () => {
|
|||
const on = getComputedStyle(Cypress.$('[data-testid="outline-on"]')[0]);
|
||||
const off = getComputedStyle($off[0]);
|
||||
|
||||
expect(off.color, 'text colour survives').to.equal(on.color);
|
||||
expect(off.borderTopColor, 'border colour survives').to.equal(on.borderTopColor);
|
||||
expect(off.color, 'text color survives').to.equal(on.color);
|
||||
expect(off.borderTopColor, 'border color survives').to.equal(on.borderTopColor);
|
||||
});
|
||||
});
|
||||
|
||||
it('does the same for icon-only buttons', () => {
|
||||
// The file browser's row actions are IconButtons, and they disable the same way.
|
||||
cy.mountUI(<div>
|
||||
{/* Filled, so there is a colour to lose. IconButton defaults to Mantine's
|
||||
{/* Filled, so there is a color to lose. IconButton defaults to Mantine's
|
||||
`default` variant, which is a white face, and comparing white with white
|
||||
would prove nothing. */}
|
||||
<IconButton icon='trash' label='Delete' color='red' variant='filled'
|
||||
|
|
@ -3332,7 +3332,7 @@ describe('the search field\'s clear button is attached to it', () => {
|
|||
});
|
||||
|
||||
it('has square corners, so nothing shows through at the field edge', () => {
|
||||
// A rounded ActionIcon stretched flush to a square field leaves panel-coloured wedges
|
||||
// A rounded ActionIcon stretched flush to a square field leaves panel-colored wedges
|
||||
// at the corners. Nothing sets this here: the theme zeroes every radius, and this
|
||||
// records that the weld depends on it.
|
||||
mountField();
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
* Only `filled` gets the first rule: an outline or subtle button sets `--button-bg` to
|
||||
* `transparent`, and there the surface behind the button is still the right answer.
|
||||
* `:not([data-variant])` is that same case -- Mantine omits the attribute entirely when a
|
||||
* call site names a colour but no variant, which is how every filled button here is
|
||||
* call site names a color but no variant, which is how every filled button here is
|
||||
* written, and keying on the attribute alone matched none of them. `--button-bg` itself
|
||||
* is absent when a button names neither, hence the fallback chain.
|
||||
*/
|
||||
|
|
@ -377,7 +377,7 @@ html[data-theme="amber"] .wrolpi-searchbox-control:focus-within {
|
|||
* claims to set -- true today only because the field happens to be the taller of the two.
|
||||
*
|
||||
* The corner radius needs no rule: the theme zeroes every `--mantine-radius-*`, so there are
|
||||
* no rounded corners to leave panel-coloured wedges against the square field.
|
||||
* no rounded corners to leave panel-colored wedges against the square field.
|
||||
*/
|
||||
html .wrolpi-searchbox-clear {
|
||||
align-self: stretch;
|
||||
|
|
@ -723,7 +723,7 @@ html[data-theme="amber"] .wrolpi-path-input-control:focus-within {
|
|||
* `--label-text` is set per-tag by Tags.js, which calculates black or light text against
|
||||
* the user's chosen fill. The fallback covers every label whose fill is a theme token.
|
||||
* It is a variable rather than an inline `color` so that a theme which discards the fill
|
||||
* can also discard the text colour calculated for it.
|
||||
* can also discard the text color calculated for it.
|
||||
*/
|
||||
color: var(--label-text, var(--btn-text));
|
||||
border: 1px solid transparent;
|
||||
|
|
@ -733,7 +733,7 @@ html[data-theme="amber"] .wrolpi-path-input-control:focus-within {
|
|||
/*
|
||||
* A tag is a label with a physical shape: a pointed left edge and an eyelet, like a tag
|
||||
* tied to a garment. The point is a square rotated 45 degrees behind the left edge -- its
|
||||
* right half sits under the body in the same colour and merges, leaving the left half
|
||||
* right half sits under the body in the same color and merges, leaving the left half
|
||||
* showing as the point. At 20px a side its diagonal is 28px, which is the body's height,
|
||||
* so the two edges of the point meet the body exactly at its corners.
|
||||
*
|
||||
|
|
@ -799,7 +799,7 @@ html[data-theme="amber"] .wrolpi-path-input-control:focus-within {
|
|||
|
||||
/*
|
||||
* White and black labels would vanish into the surface they sit on. They set the text
|
||||
* colour through `--label-text` rather than `color` for the same reason the per-tag colour
|
||||
* color through `--label-text` rather than `color` for the same reason the per-tag color
|
||||
* does: setting `color` here outranked the base rule, so amber -- which replaces every fill
|
||||
* with its own and has no `color` rule of its own -- was left painting `--white` on `--text`,
|
||||
* light orange on orange.
|
||||
|
|
@ -937,20 +937,20 @@ html[data-theme="night"] .wrolpi-tag::before {
|
|||
|
||||
/*
|
||||
* Night and amber are single-hue by definition, and a call site may legitimately want
|
||||
* many distinguishable colours — the Flasher's chip-family badges set `--label-color`
|
||||
* inline from a twelve-colour palette that no four tokens can express.
|
||||
* many distinguishable colors — the Flasher's chip-family badges set `--label-color`
|
||||
* inline from a twelve-color palette that no four tokens can express.
|
||||
*
|
||||
* In those themes the palette is overridden back to the theme's own text colour, so a
|
||||
* In those themes the palette is overridden back to the theme's own text color, so a
|
||||
* label cannot put a green or blue pixel on a night-adapted screen. `!important` is
|
||||
* required because the palette arrives as an inline custom property, which a normal
|
||||
* rule cannot outrank. The labels stop being distinguishable by colour, which is the
|
||||
* rule cannot outrank. The labels stop being distinguishable by color, which is the
|
||||
* correct trade: the theme's whole purpose is that no other hue reaches the eye.
|
||||
*/
|
||||
html[data-theme="night"] .wrolpi-label,
|
||||
html[data-theme="amber"] .wrolpi-label {
|
||||
--label-color: var(--text) !important;
|
||||
/*
|
||||
* The per-tag text colour goes with the fill it was calculated against. Tags.js works
|
||||
* The per-tag text color goes with the fill it was calculated against. Tags.js works
|
||||
* out black-or-light from the user's chosen hex, and once that hex is discarded the
|
||||
* answer is not just useless but wrong: amber was painting `#dddddd` on `#ff9500`, about
|
||||
* 1.9:1 and a neutral grey in a theme whose whole point is that nothing else is. Night,
|
||||
|
|
@ -964,7 +964,7 @@ html[data-theme="amber"] .wrolpi-label {
|
|||
|
||||
/* ----------------------------------------------------------------- Toasts */
|
||||
/*
|
||||
* Notifications take their colours from our tokens rather than from Mantine's colour scheme.
|
||||
* Notifications take their colors from our tokens rather than from Mantine's color scheme.
|
||||
*
|
||||
* Mantine paints a dark-scheme notification title with `--mantine-color-white`, and the
|
||||
* bridge aliases that to `--btn-text` -- deliberately, because it is the text drawn on a
|
||||
|
|
@ -1180,7 +1180,7 @@ html .mantine-Notification-closeButton:hover {
|
|||
* 5.49:1 in light (the brightness filter below is what buys that) but 2.35 in
|
||||
* dark, 2.48 in night and 2.74 in amber; against the empty track it is 5.2-15:1
|
||||
* everywhere. Reaching AA over the fill needs the label drawn twice, clipped to
|
||||
* the fill boundary, so each half gets its own colour -- and in night not even
|
||||
* the fill boundary, so each half gets its own color -- and in night not even
|
||||
* that would do it, since `--text` is only ~4:1 against pure black and the fill
|
||||
* is lighter than black by definition. Left as it is, deliberately, rather than
|
||||
* asserted at a threshold chosen to pass.
|
||||
|
|
@ -1314,12 +1314,12 @@ html[data-theme="amber"] .wrolpi-card-tag {
|
|||
/*
|
||||
* The group is spacing and nothing else: no frame, no rules between cells, and
|
||||
* no surface of its own, so the statistics sit on whatever they are dropped onto
|
||||
* -- the page in one place, a Panel in another -- and pick up its colour.
|
||||
* -- the page in one place, a Panel in another -- and pick up its color.
|
||||
*
|
||||
* That also settles a question this went round twice on. Ruling the cells needs
|
||||
* a technique that survives wrapping, because `auto-fit` decides the number of
|
||||
* rows at paint time: a `border-right` per cell rules columns but not rows, and
|
||||
* a border-coloured background behind a 1px gap rules both but paints every
|
||||
* a border-colored background behind a 1px gap rules both but paints every
|
||||
* track no cell landed in. With no rules at all, neither problem exists.
|
||||
*
|
||||
* The gap is wider across than down. Two statistics side by side have nothing
|
||||
|
|
@ -1378,7 +1378,7 @@ html[data-theme="amber"] .wrolpi-card-tag {
|
|||
|
||||
/* ---------------------------------------------------------------- Status */
|
||||
/*
|
||||
* One rule for four states in four themes. The colour is the role the component
|
||||
* One rule for four states in four themes. The color is the role the component
|
||||
* chose, and the role already knows what it looks like here -- which is what
|
||||
* replaced the three `html[data-theme="night"]` overrides that used to live below.
|
||||
*/
|
||||
|
|
@ -1392,7 +1392,7 @@ html[data-theme="amber"] .wrolpi-card-tag {
|
|||
}
|
||||
|
||||
/*
|
||||
* Failure is not left to colour alone. Night and amber distinguish the roles by
|
||||
* Failure is not left to color alone. Night and amber distinguish the roles by
|
||||
* brightness, and brightness is the first thing lost on a dim screen or to a user
|
||||
* who cannot separate these hues, so the loudest state also carries weight.
|
||||
*/
|
||||
|
|
@ -1502,21 +1502,21 @@ html[data-theme="amber"] .wrolpi-th-sort:focus-visible {
|
|||
}
|
||||
|
||||
/*
|
||||
* A disabled button keeps its own colour.
|
||||
* A disabled button keeps its own color.
|
||||
*
|
||||
* Mantine repaints one flat grey -- background, text and border all replaced with
|
||||
* `--mantine-color-disabled`. Semantic kept the colour and dropped the whole control to
|
||||
* `--mantine-color-disabled`. Semantic kept the color and dropped the whole control to
|
||||
* `opacity: 0.45`, and that is the better signal: it says "not available" without also
|
||||
* saying "no longer the delete button".
|
||||
*
|
||||
* The file browser is where the difference shows. Its footer is eight buttons, six of them
|
||||
* disabled until something is selected, so the toolbar a user meets on arriving at /files was
|
||||
* an undifferentiated grey row -- Delete, Rename, Move, Ignore and Tag all the same pixel.
|
||||
* Colour is how those are told apart at a glance.
|
||||
* Color is how those are told apart at a glance.
|
||||
*
|
||||
* Mantine leaves the variables alone and overrides only the painted properties, so the colour
|
||||
* Mantine leaves the variables alone and overrides only the painted properties, so the color
|
||||
* the call site asked for is still in `--button-bg` and can simply be reinstated. `--button-bd`
|
||||
* is a whole border shorthand, not a colour, so it is set with `border`.
|
||||
* is a whole border shorthand, not a color, so it is set with `border`.
|
||||
*
|
||||
* `:not([data-loading])` mirrors Mantine, and is not decoration. Mantine renders a loading
|
||||
* button as `disabled={disabled || loading}`, so every busy control in the app is `:disabled`
|
||||
|
|
|
|||
|
|
@ -383,7 +383,7 @@ describe('Header', () => {
|
|||
expect(container.querySelector('h3').style.marginBottom).toBe('');
|
||||
});
|
||||
|
||||
it('still colours the heading text from the colour prop', () => {
|
||||
it('still colors the heading text from the color prop', () => {
|
||||
const {container} = renderUI(<Header as='h4' color='danger'>2 items to remove</Header>);
|
||||
|
||||
expect(container.querySelector('h4')).toHaveStyle({color: 'var(--danger)'});
|
||||
|
|
@ -619,10 +619,10 @@ describe('Progress', () => {
|
|||
expect(bar).toHaveTextContent('62%');
|
||||
});
|
||||
|
||||
it('keeps its percent text readable on every bar colour', () => {
|
||||
it('keeps its percent text readable on every bar color', () => {
|
||||
/*
|
||||
* Ported from Theme.test.js, which asserted Semantic's `inverted-progress-text`
|
||||
* class across all fourteen colours and both modes. That class is gone; the
|
||||
* class across all fourteen colors and both modes. That class is gone; the
|
||||
* mechanism now is that the text always takes `--text` while light mode lightens
|
||||
* the fill beneath it, because the text sits across both the filled and unfilled
|
||||
* halves of the bar.
|
||||
|
|
@ -701,9 +701,9 @@ describe('Label as a tag', () => {
|
|||
expect(container.querySelector('.wrolpi-tag')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('leaves the text colour to the stylesheet', () => {
|
||||
// Every label routes its text colour through `--label-text` so that night and amber,
|
||||
// which replace the fill with their own, can replace the text colour with it. An
|
||||
it('leaves the text color to the stylesheet', () => {
|
||||
// Every label routes its text color through `--label-text` so that night and amber,
|
||||
// which replace the fill with their own, can replace the text color with it. An
|
||||
// inline `color` could not be overridden by either.
|
||||
const {container} = renderUI(<Label tag color='blue'>Water</Label>);
|
||||
|
||||
|
|
@ -742,18 +742,18 @@ describe('the library names severity by role, never by hue', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('nothing paints a label\'s text with an inline colour', () => {
|
||||
describe('nothing paints a label\'s text with an inline color', () => {
|
||||
/*
|
||||
* A source guard, because this defect appeared at three separate call sites and fixing the
|
||||
* first one did not fix the others.
|
||||
*
|
||||
* A label's text colour has to travel through `--label-text`. An inline `color` is a
|
||||
* A label's text color has to travel through `--label-text`. An inline `color` is a
|
||||
* declaration no stylesheet rule can outrank, so night -- which turns a label into an
|
||||
* outline over a near-black page -- keeps whatever was calculated for the fill it just
|
||||
* discarded. A bright fill leaves black text on near-black: the tag reads as an empty
|
||||
* outline, and nothing fails.
|
||||
*
|
||||
* The three were the tag chip, the tag edit preview (the very preview of the colour being
|
||||
* The three were the tag chip, the tag edit preview (the very preview of the color being
|
||||
* chosen), and the Flasher's chip badges. The stylesheet comment explaining the night and
|
||||
* amber override already named the Flasher, and it was still missed.
|
||||
*/
|
||||
|
|
@ -881,7 +881,7 @@ describe('Table', () => {
|
|||
|
||||
describe('Card', () => {
|
||||
it('draws a mimetype accent from a token, not a hex', () => {
|
||||
// File cards carry the mimetype's colour on their top edge so a grid of results is
|
||||
// File cards carry the mimetype's color on their top edge so a grid of results is
|
||||
// scannable by kind. Semantic did this with `<Card color='violet'>`; two migrated
|
||||
// files had dropped the accent because our Card had no equivalent.
|
||||
const {container} = renderUI(<Card title='Water Storage.pdf' color='red'/>);
|
||||
|
|
@ -889,7 +889,7 @@ describe('Card', () => {
|
|||
expect(container.firstChild).toHaveStyle({borderBottom: '3px solid var(--red)'});
|
||||
});
|
||||
|
||||
it('has no accent when no colour is given', () => {
|
||||
it('has no accent when no color is given', () => {
|
||||
const {container} = renderUI(<Card title='Plain'/>);
|
||||
|
||||
expect(container.firstChild.style.borderBottom).toBe('');
|
||||
|
|
@ -959,7 +959,7 @@ describe('Statistic', () => {
|
|||
expect(screen.getByText('Free space')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('colours the value from a token, not a hex', () => {
|
||||
it('colors the value from a token, not a hex', () => {
|
||||
// Status turns load, temperature and IO wait red or orange. A hex here would not
|
||||
// remap in night mode, putting a green or orange pixel on a red-only screen.
|
||||
const {container} = renderUI(<Statistic value='3.9' label='1 Min. Load' color='red'/>);
|
||||
|
|
@ -967,7 +967,7 @@ describe('Statistic', () => {
|
|||
expect(container.querySelector('.wrolpi-statistic-value')).toHaveStyle({color: 'var(--red)'});
|
||||
});
|
||||
|
||||
it('leaves the value in body text when no colour is given', () => {
|
||||
it('leaves the value in body text when no color is given', () => {
|
||||
const {container} = renderUI(<Statistic value='0.4' label='1 Min. Load'/>);
|
||||
|
||||
expect(container.querySelector('.wrolpi-statistic-value').style.color).toBe('');
|
||||
|
|
@ -1019,7 +1019,7 @@ describe('StatisticGroup', () => {
|
|||
|
||||
it('draws no chrome of its own, in markup or in style', () => {
|
||||
// The group is spacing only: the statistics sit on whatever surface they were dropped
|
||||
// onto and take its colour. An inline border or background here would override the
|
||||
// onto and take its color. An inline border or background here would override the
|
||||
// stylesheet and could not be themed, so nothing may set one.
|
||||
const {container} = renderUI(<StatisticGroup>
|
||||
<Statistic value='1,432' label='Videos'/>
|
||||
|
|
@ -1143,8 +1143,8 @@ describe('Loader', () => {
|
|||
expect(screen.getByLabelText('Fetching channels')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('takes its colour from a token rather than a fixed value', () => {
|
||||
// A hex here would be one colour in all four themes, and a blue one in night mode.
|
||||
it('takes its color from a token rather than a fixed value', () => {
|
||||
// A hex here would be one color in all four themes, and a blue one in night mode.
|
||||
const {container} = renderUI(<Loader/>);
|
||||
|
||||
expect(container.querySelector('.mantine-Loader-root').style.getPropertyValue('--loader-color'))
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ export function renderWithProviders(
|
|||
if (route) window.history.pushState({}, '', route);
|
||||
|
||||
/*
|
||||
* The theme decides the colour scheme, rather than the two being set independently.
|
||||
* The theme decides the color scheme, rather than the two being set independently.
|
||||
*
|
||||
* `inverted` used to drive Mantine's scheme on its own, so asking for night or amber
|
||||
* through the theme option produced a tree that reported a dark theme while Mantine
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {amberTheme, darkTheme, lightTheme, nightTheme} from './themes/names';
|
|||
*
|
||||
* Both of these cover mistakes it shipped with. `mockModule` had no caller at all, so its
|
||||
* documented usage was never once executed and could not have worked; and the theme option
|
||||
* moved ThemeContext without moving Mantine's colour scheme, so asking for night produced a
|
||||
* moved ThemeContext without moving Mantine's color scheme, so asking for night produced a
|
||||
* tree that reported night and rendered light -- a state production never reaches, which is
|
||||
* the one thing a test harness must not invent.
|
||||
*/
|
||||
|
|
@ -18,7 +18,7 @@ const ThemeProbe = () => {
|
|||
return <div data-testid='probe'>{`${theme}:${isDark}`}</div>;
|
||||
};
|
||||
|
||||
describe('render keeps the theme and the colour scheme together', () => {
|
||||
describe('render keeps the theme and the color scheme together', () => {
|
||||
// The dark-background themes, which is what Mantine has to be told about. night and amber
|
||||
// are the interesting ones: neither is ever chosen by prefers-color-scheme, so nothing else
|
||||
// would put Mantine's own components on a dark scheme for them.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
* its existing callers are unaffected.
|
||||
*/
|
||||
|
||||
/** A hex colour as [r, g, b], accepting `#rgb`, `#rrggbb`, and `#rrggbbaa`. */
|
||||
/** A hex color as [r, g, b], accepting `#rgb`, `#rrggbb`, and `#rrggbbaa`. */
|
||||
export function hexToRGBArray(color: string): number[] | undefined {
|
||||
let hex = (typeof color === 'string' ? color : '').trim().replace(/^#/, '');
|
||||
if (hex.length === 3 || hex.length === 4) {
|
||||
|
|
@ -24,19 +24,19 @@ export function hexToRGBArray(color: string): number[] | undefined {
|
|||
}
|
||||
|
||||
/**
|
||||
* Any colour these measurements can actually read, as [r, g, b] — or undefined.
|
||||
* Any color these measurements can actually read, as [r, g, b] — or undefined.
|
||||
*
|
||||
* Undefined matters as much as the value. `relativeLuminance` used to return 0 for
|
||||
* everything it could not parse, which is indistinguishable from black: a token written as
|
||||
* `rgb(90, 79, 168)` measured as pure black, so the "best" foreground was chosen against a
|
||||
* colour the theme does not contain, and the wrong one was frozen into the bar's inline
|
||||
* color the theme does not contain, and the wrong one was frozen into the bar's inline
|
||||
* style. A caller that cannot tell "unreadable" from "black" cannot fall back, and the
|
||||
* custom YAML themes ahead of us are exactly where a non-hex token will first appear.
|
||||
*
|
||||
* Alpha is deliberately dropped rather than rejected: WCAG contrast is defined on composited
|
||||
* colour, and what a translucent value composites over is not knowable here. Every token in
|
||||
* color, and what a translucent value composites over is not knowable here. Every token in
|
||||
* tokens.css is opaque, so this only arises for a hand-written theme, where measuring the
|
||||
* colour itself is a better answer than refusing.
|
||||
* color itself is a better answer than refusing.
|
||||
*/
|
||||
export function parseColor(color: string): number[] | undefined {
|
||||
const value = (typeof color === 'string' ? color : '').trim();
|
||||
|
|
@ -58,7 +58,7 @@ export function parseColor(color: string): number[] | undefined {
|
|||
return channels.map(channel => Math.min(255, Math.max(0, channel)));
|
||||
}
|
||||
|
||||
// A named colour, `hsl()`, `color-mix()`, or an unresolved `var()`. Not measurable
|
||||
// A named color, `hsl()`, `color-mix()`, or an unresolved `var()`. Not measurable
|
||||
// here, and saying so is the point.
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ export function parseColor(color: string): number[] | undefined {
|
|||
*
|
||||
* The linearisation is the part that matters. Weighting the gamma-encoded bytes directly
|
||||
* is a rough approximation that misjudges mid-tone blues and purples badly enough to pick
|
||||
* the wrong text colour for them.
|
||||
* the wrong text color for them.
|
||||
*/
|
||||
export function relativeLuminance(color: string | number[]): number {
|
||||
const rgb = (typeof color === 'string') ? parseColor(color) : color;
|
||||
|
|
@ -85,13 +85,13 @@ export function relativeLuminance(color: string | number[]): number {
|
|||
return (0.2126 * linear[0]) + (0.7152 * linear[1]) + (0.0722 * linear[2]);
|
||||
}
|
||||
|
||||
/** WCAG contrast ratio between two colours, from 1:1 (identical) to 21:1 (black on white). */
|
||||
/** WCAG contrast ratio between two colors, from 1:1 (identical) to 21:1 (black on white). */
|
||||
export function contrastRatio(a: string | number[], b: string | number[]): number {
|
||||
const luminances = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
|
||||
return (luminances[0] + 0.05) / (luminances[1] + 0.05);
|
||||
}
|
||||
|
||||
/** Whether a colour can be measured at all, as distinct from measuring as black. */
|
||||
/** Whether a color can be measured at all, as distinct from measuring as black. */
|
||||
export const isMeasurable = (color: string): boolean => parseColor(color) !== undefined;
|
||||
|
||||
/**
|
||||
|
|
@ -100,7 +100,7 @@ export const isMeasurable = (color: string): boolean => parseColor(color) !== un
|
|||
* Measures every option rather than comparing a brightness figure to a threshold. A
|
||||
* threshold picks the worse of two options for every mid-tone -- which is the whole
|
||||
* defect this exists to avoid -- and a mid-tone is exactly what a user picks when they
|
||||
* choose a navbar colour.
|
||||
* choose a navbar color.
|
||||
*
|
||||
* Ties go to the earlier candidate, so a caller can order them by preference.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const tokenColor = (token: string): MantineColorsTuple =>
|
|||
Array(10).fill(`var(--${token})`) as unknown as MantineColorsTuple;
|
||||
|
||||
/**
|
||||
* The semantic roles. A component asks for what a colour MEANS; the theme decides
|
||||
* The semantic roles. A component asks for what a color MEANS; the theme decides
|
||||
* how that looks. Registered as Mantine colors too, so `color='danger'` works on any
|
||||
* Mantine component the same way `color='red'` does. See the note in tokens.css --
|
||||
* in night and amber these are brightnesses, not hues.
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ describe('the monochrome themes get an achromatic basemap', () => {
|
|||
*
|
||||
* Both media filters are a pure luminance projection -- the same `0.2126 0.7152 0.0722`
|
||||
* row -- so they keep brightness and discard hue. A basemap that encodes anything in
|
||||
* hue loses it: `dark` marks water and parks with colour, and after filtering those
|
||||
* hue loses it: `dark` marks water and parks with color, and after filtering those
|
||||
* features sit at whatever brightness their hue happened to carry. An achromatic
|
||||
* basemap has nothing to lose, so the designer's hierarchy arrives intact.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const explicitOnlyThemes: ThemeName[] = [nightTheme, amberTheme];
|
|||
/*
|
||||
* Themes built on a single hue, which is the constraint the semantic roles exist for.
|
||||
*
|
||||
* These have no second colour to spend, so a role cannot be a hue there -- it has to be
|
||||
* These have no second color to spend, so a role cannot be a hue there -- it has to be
|
||||
* a step on a brightness ramp, and `--orange` being byte-identical to `--text` in night
|
||||
* is what that costs when it is ignored. Light and dark have hues and are free to use
|
||||
* them, so severity does NOT read as brightness there and must not be asserted to.
|
||||
|
|
@ -56,7 +56,7 @@ export const isDarkTheme = (theme: ThemeName): boolean => darkThemes.includes(th
|
|||
* projection -- the same `0.2126 0.7152 0.0722` row -- so they keep brightness and throw
|
||||
* hue away. Filtering `black` is therefore a hue rotation and nothing else: all thirteen
|
||||
* of its greys were authored as a monochrome hierarchy and every one survives. `dark` has
|
||||
* nineteen colours but four of them are hued (water, parks, up to 25 chroma), and those
|
||||
* nineteen colors but four of them are hued (water, parks, up to 25 chroma), and those
|
||||
* distinctions do not survive at all -- the features land at whatever brightness their hue
|
||||
* happened to carry rather than one anybody chose. `black` is also half the light:
|
||||
* earth L0.007 against dark's L0.014, which is the point of a night-vision theme.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {semanticUIColorMap} from '../components/Vars';
|
|||
/*
|
||||
* The navbar foreground, measured against the palette that actually ships.
|
||||
*
|
||||
* The bar's background is the one colour a user picks by name, so there are twelve of them
|
||||
* The bar's background is the one color a user picks by name, so there are twelve of them
|
||||
* per theme and forty-eight in total -- too many to eyeball, and the reason the bar drew
|
||||
* near-black text on night's #451212 for as long as it did. The tokens are parsed out of
|
||||
* tokens.css rather than restated here: a list restated here is a list that agrees with
|
||||
|
|
@ -51,17 +51,17 @@ describe('the navbar palette', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('offers exactly the colours the favicon has an icon for', () => {
|
||||
it('offers exactly the colors the favicon has an icon for', () => {
|
||||
/*
|
||||
* `semanticUIColorMap` supplies the fixed hexes for `/favicon-<colour>.svg` and the
|
||||
* `semanticUIColorMap` supplies the fixed hexes for `/favicon-<color>.svg` and the
|
||||
* `theme-color` meta tag -- a file on disk and OS chrome, neither of which can take a
|
||||
* token. A colour offered here with no icon there falls back to violet, so the bar
|
||||
* token. A color offered here with no icon there falls back to violet, so the bar
|
||||
* and the browser tab would disagree.
|
||||
*/
|
||||
expect([...navColorNames].sort()).toEqual(Object.keys(semanticUIColorMap).sort());
|
||||
});
|
||||
|
||||
it('defaults to a colour it offers', () => {
|
||||
it('defaults to a color it offers', () => {
|
||||
expect(navColorNames).toContain(defaultNavColor);
|
||||
});
|
||||
});
|
||||
|
|
@ -71,7 +71,7 @@ describe('the navbar foreground', () => {
|
|||
const candidatesOf = (palette) =>
|
||||
[palette['--btn-text'], palette['--black'], palette['--white']];
|
||||
|
||||
it('takes a colour the theme itself supplies, never a literal black or white', () => {
|
||||
it('takes a color the theme itself supplies, never a literal black or white', () => {
|
||||
/*
|
||||
* A literal white pixel in night mode undoes the user's dark adaptation, which is the
|
||||
* one thing that theme exists to prevent. Night's "white" is #ff8a8a and its "black"
|
||||
|
|
@ -88,7 +88,7 @@ describe('the navbar foreground', () => {
|
|||
|
||||
it('picks the best of them, every time', () => {
|
||||
// The invariant. A brightness threshold picks the worse option for every mid-tone,
|
||||
// and a mid-tone is exactly what a navbar colour is.
|
||||
// and a mid-tone is exactly what a navbar color is.
|
||||
everyCombination().forEach(({theme, navColor, background, color, palette}) => {
|
||||
const chosenRatio = contrastRatio(color, background);
|
||||
const rejected = candidatesOf(palette)
|
||||
|
|
@ -126,7 +126,7 @@ describe('the navbar foreground', () => {
|
|||
/*
|
||||
* 3:1 is the WCAG floor for large text and for graphical objects, which is what the
|
||||
* bar's icons are. Twelve combinations were below it -- night's brown at 1.33:1 was
|
||||
* a glyph the same colour as the bar behind it.
|
||||
* a glyph the same color as the bar behind it.
|
||||
*/
|
||||
const failing = everyCombination()
|
||||
.filter(({ratio}) => ratio < 3)
|
||||
|
|
@ -153,7 +153,7 @@ describe('what measurement cannot fix', () => {
|
|||
* Ten of the forty-eight combinations still sit under 4.5:1, and no choice of
|
||||
* foreground will lift them, because the shortfall is in the BACKGROUND.
|
||||
*
|
||||
* A monochrome theme resolves all twelve colour names onto one red (or amber) ramp, and
|
||||
* A monochrome theme resolves all twelve color names onto one red (or amber) ramp, and
|
||||
* six of night's land in the middle of it -- #b03030 is 3.27:1 from night's darkest red
|
||||
* and 2.52:1 from its brightest, so the best available option is the one this picks and
|
||||
* it is still short. There is no foreground in a one-hue palette that reads on a
|
||||
|
|
@ -230,13 +230,13 @@ describe('when the tokens cannot be read', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('falls back for an unknown colour too', () => {
|
||||
it('falls back for an unknown color too', () => {
|
||||
// `nav_color` comes from a config file, which a user edits by hand.
|
||||
expect(navColorsFrom(() => '', 'chartreuse').background)
|
||||
.toEqual(`var(--${defaultNavColor})`);
|
||||
});
|
||||
|
||||
it('resolves an unknown colour to the default when tokens ARE readable', () => {
|
||||
it('resolves an unknown color to the default when tokens ARE readable', () => {
|
||||
const read = readerFor('light');
|
||||
expect(navColorsFrom(read, 'chartreuse').background)
|
||||
.toEqual(paletteOf('light')[`--${defaultNavColor}`]);
|
||||
|
|
@ -249,9 +249,9 @@ describe('a token this cannot measure', () => {
|
|||
* us -- the first place a palette will be written by hand, and the first place something
|
||||
* other than a hex will appear.
|
||||
*
|
||||
* The failure it guards was silent and worse than doing nothing. An unparseable colour
|
||||
* The failure it guards was silent and worse than doing nothing. An unparseable color
|
||||
* measured as luminance zero, indistinguishable from black, so the bar chose its
|
||||
* foreground against a colour the theme does not contain and then froze the wrong hex
|
||||
* foreground against a color the theme does not contain and then froze the wrong hex
|
||||
* into an inline style, where CSS could no longer correct it. Falling back leaves the
|
||||
* bar painted by CSS: wrong about the foreground at worst, rather than about both.
|
||||
*/
|
||||
|
|
@ -263,8 +263,8 @@ describe('a token this cannot measure', () => {
|
|||
return (token) => palette[token] || '';
|
||||
};
|
||||
|
||||
it('measures rgb() as readily as hex, since both are colours', () => {
|
||||
// `--violet` is #5c4fa8 in light. The same colour, written the other way, must
|
||||
it('measures rgb() as readily as hex, since both are colors', () => {
|
||||
// `--violet` is #5c4fa8 in light. The same color, written the other way, must
|
||||
// produce the same answer rather than falling back.
|
||||
const asHex = navColorsFrom(readerFor('light'), 'violet');
|
||||
const asRgb = navColorsFrom(readerWith({'--violet': 'rgb(92, 79, 168)'}), 'violet');
|
||||
|
|
@ -312,7 +312,7 @@ describe('a token this cannot measure', () => {
|
|||
expect([hexPalette()['--btn-text'], hexPalette()['--black']]).toContain(colors.color);
|
||||
});
|
||||
|
||||
it('is a real constraint: an unreadable colour is NOT treated as black', () => {
|
||||
it('is a real constraint: an unreadable color is NOT treated as black', () => {
|
||||
/*
|
||||
* Without this the fallback could be removed and every case above would still pass
|
||||
* on `relativeLuminance` returning zero -- which is precisely the bug, because zero
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import {bestForeground, contrastRatio, isMeasurable} from './contrast';
|
|||
|
||||
|
||||
/*
|
||||
* The navigation bar's colours.
|
||||
* The navigation bar's colors.
|
||||
*
|
||||
* The bar's background is the one colour in the interface the USER picks (Settings ->
|
||||
* navbar colour), and it is picked by hue name -- `violet`, `olive`, `brown` -- which each
|
||||
* The bar's background is the one color in the interface the USER picks (Settings ->
|
||||
* navbar color), and it is picked by hue name -- `violet`, `olive`, `brown` -- which each
|
||||
* theme then resolves to its own hex. So the bar has a background the theme cannot know in
|
||||
* advance, and its links and status icons have to stay legible on all twelve of them in all
|
||||
* four themes: forty-eight combinations, not one.
|
||||
*
|
||||
* It used to draw them all with the single `--btn-text` token, which is white in light and
|
||||
* near-black in the other three. Near-black on night's `--brown` (#451212) is 1.33:1 and on
|
||||
* amber's (#452708) 1.48:1 -- nine of night's twelve colours and seven of amber's sat below
|
||||
* amber's (#452708) 1.48:1 -- nine of night's twelve colors and seven of amber's sat below
|
||||
* 4.5:1, which is why the bar's icons were unreadable. `--btn-text` is the right token for
|
||||
* a button, whose fill the theme chooses; it was never the right one here.
|
||||
*
|
||||
|
|
@ -23,27 +23,27 @@ import {bestForeground, contrastRatio, isMeasurable} from './contrast';
|
|||
* night's "white" is #ff8a8a, a bright red, and amber's is #ffc875.
|
||||
*
|
||||
* Measuring at runtime rather than shipping a paired `--on-red`/`--on-olive` token for every
|
||||
* colour also means a theme only has to supply a palette: the correct foreground for each
|
||||
* color also means a theme only has to supply a palette: the correct foreground for each
|
||||
* entry falls out of it, with nothing extra to declare and nothing to keep in step.
|
||||
*
|
||||
* What is NOT measured, and should be: the warning indicators. NavBar still picks their
|
||||
* colour from `conflictingColors`, a hardcoded list of navbar names that swaps `yellow`/`red`
|
||||
* color from `conflictingColors`, a hardcoded list of navbar names that swaps `yellow`/`red`
|
||||
* for `null`/`black` on the five bars those hues would blend into. It is the same defect
|
||||
* this replaced -- a name list standing in for a measurement -- and it is worse than the one
|
||||
* fixed here rather than better: `--danger` and `--warning` measure 1.0-1.8:1 against the bar
|
||||
* in EVERY theme, light and dark included, because a red icon on a red bar is a red icon on a
|
||||
* red bar. It is left alone deliberately, because the fix is not a better colour. Those
|
||||
* red bar. It is left alone deliberately, because the fix is not a better color. Those
|
||||
* icons have to stay distinguishable from each other AND from the bar's own text, and in a
|
||||
* monochrome theme there is no third brightness to spend, so severity has to stop being a
|
||||
* hue -- a filled badge, or a shape. That is a redesign of how severity reads, not a
|
||||
* substitution, and it is not this change.
|
||||
*
|
||||
* /theme-sample draws the indicators as they are, taking the measured bar colour, which is
|
||||
* /theme-sample draws the indicators as they are, taking the measured bar color, which is
|
||||
* what the bar does when nothing is wrong. It does not stage a fake overheating Pi to
|
||||
* exhibit a fault nobody has agreed how to fix.
|
||||
*
|
||||
* Two limits on the measuring that IS done here, both of which the custom YAML themes will
|
||||
* meet before anything else does. A token has to be a colour these measurements can read -- hex or `rgb()`; anything
|
||||
* meet before anything else does. A token has to be a color these measurements can read -- hex or `rgb()`; anything
|
||||
* else falls back to CSS rather than being measured (see navColorsFrom). And re-measurement
|
||||
* is triggered by `data-theme` changing, which is how a theme is applied today; a future
|
||||
* editor that patches custom properties in place without touching the attribute would leave
|
||||
|
|
@ -51,7 +51,7 @@ import {bestForeground, contrastRatio, isMeasurable} from './contrast';
|
|||
*/
|
||||
|
||||
/**
|
||||
* The colours offered for the bar, in picker order.
|
||||
* The colors offered for the bar, in picker order.
|
||||
*
|
||||
* The same keys as `semanticUIColorMap` in components/Vars.js, which maps them to the fixed
|
||||
* hexes used for the favicon and the mobile status bar (those are files and OS chrome, so
|
||||
|
|
@ -65,9 +65,9 @@ export const navColorNames: string[] = [
|
|||
export const defaultNavColor = 'violet';
|
||||
|
||||
export interface NavColors {
|
||||
/** CSS colour for the bar itself. */
|
||||
/** CSS color for the bar itself. */
|
||||
background: string;
|
||||
/** CSS colour its text and icons take. */
|
||||
/** CSS color its text and icons take. */
|
||||
color: string;
|
||||
/**
|
||||
* Measured contrast of `color` on `background`, or null when the tokens could not be
|
||||
|
|
@ -78,7 +78,7 @@ export interface NavColors {
|
|||
}
|
||||
|
||||
/**
|
||||
* The bar's colours, given a way to read theme tokens.
|
||||
* The bar's colors, given a way to read theme tokens.
|
||||
*
|
||||
* Takes a reader rather than an element so it can be measured against the real palette in a
|
||||
* unit test without a browser -- the shipped tokens are parsed out of tokens.css and handed
|
||||
|
|
@ -106,8 +106,8 @@ export function navColorsFrom(read: (name: string) => string, navColor: string):
|
|||
* or a very early render -- and the reader returns empty strings. The other is a
|
||||
* token this cannot parse: a hand-written theme writing `rgb(90 79 168)` or
|
||||
* `color-mix(...)` rather than a hex. That one used to be silent and worse than
|
||||
* useless: an unparseable colour measured as luminance zero, indistinguishable from
|
||||
* black, so the bar picked its foreground against a colour the theme does not contain
|
||||
* useless: an unparseable color measured as luminance zero, indistinguishable from
|
||||
* black, so the bar picked its foreground against a color the theme does not contain
|
||||
* and then froze the wrong hex into an inline style, where CSS could not correct it.
|
||||
*
|
||||
* Falling back to the tokens themselves is the pre-measurement behaviour: the bar is
|
||||
|
|
@ -121,7 +121,7 @@ export function navColorsFrom(read: (name: string) => string, navColor: string):
|
|||
return {background, color, ratio: contrastRatio(color, background)};
|
||||
}
|
||||
|
||||
/** The bar's colours read from a live document. */
|
||||
/** The bar's colors read from a live document. */
|
||||
export function resolveNavColors(
|
||||
navColor: string,
|
||||
element: Element = document.documentElement,
|
||||
|
|
@ -131,7 +131,7 @@ export function resolveNavColors(
|
|||
}
|
||||
|
||||
/**
|
||||
* The inline style for a bar painted in these colours.
|
||||
* The inline style for a bar painted in these colors.
|
||||
*
|
||||
* Shared by the real navigation bar and the /theme-sample gallery, so a sample cannot end
|
||||
* up demonstrating a bar the app does not actually draw.
|
||||
|
|
@ -142,7 +142,7 @@ export function navBarStyle(colors: NavColors): React.CSSProperties {
|
|||
color: colors.color,
|
||||
/*
|
||||
* The hotspot glyph stacks two icons, and the corner one paints a disc of its
|
||||
* surface behind itself. The bar's colour is the user's choice, so it has to be
|
||||
* surface behind itself. The bar's color is the user's choice, so it has to be
|
||||
* handed down rather than looked up from a token.
|
||||
*/
|
||||
'--icon-stack-bg': colors.background,
|
||||
|
|
@ -150,7 +150,7 @@ export function navBarStyle(colors: NavColors): React.CSSProperties {
|
|||
}
|
||||
|
||||
/**
|
||||
* The bar's colours, kept current as the theme changes.
|
||||
* The bar's colors, kept current as the theme changes.
|
||||
*
|
||||
* Watches the `data-theme` attribute rather than subscribing to ThemeContext, because the
|
||||
* attribute is what the tokens actually key off and it is stamped by ThemeProvider in an
|
||||
|
|
@ -164,7 +164,7 @@ export function navBarStyle(colors: NavColors): React.CSSProperties {
|
|||
* This assumes token VALUES only change when `data-theme` does, which holds for every way a
|
||||
* theme is applied today. It will not hold for a custom-theme editor that rewrites custom
|
||||
* properties in place: the measured pair is a snapshot of hexes, so the bar would keep the
|
||||
* old one until the theme name or the chosen colour changed. When that editor exists it
|
||||
* old one until the theme name or the chosen color changed. When that editor exists it
|
||||
* should either re-stamp the attribute or announce the change for this to observe -- there
|
||||
* is no event for "a custom property was assigned" to watch instead.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ html[data-theme="light"] {
|
|||
/*
|
||||
* Semantic roles.
|
||||
*
|
||||
* What a colour MEANS, separate from what hue it happens to be. A call site asks
|
||||
* What a color MEANS, separate from what hue it happens to be. A call site asks
|
||||
* for `--danger`, not `--red`, and each theme decides how danger looks.
|
||||
*
|
||||
* This matters because two of the four themes have no hues to spend. Night is red
|
||||
|
|
@ -73,7 +73,7 @@ html[data-theme="light"] {
|
|||
* kinds looked the same in night, and why a warning and an error were the same pixel.
|
||||
*
|
||||
* In EVERY theme:
|
||||
* - the five roles resolve to five distinct colours;
|
||||
* - the five roles resolve to five distinct colors;
|
||||
* - every role except neutral clears 3:1 against the panel. Neutral sits below the
|
||||
* rest deliberately -- for `pending` and disabled, being dim IS the signal.
|
||||
*
|
||||
|
|
@ -101,9 +101,9 @@ html[data-theme="light"] {
|
|||
*
|
||||
* `ui-layout.cy.js` asserts exactly this split, per theme, in a real browser.
|
||||
*
|
||||
* `--primary` is deliberately NOT one of these. "The colour of a primary button" and
|
||||
* "the colour that means informational" are different jobs, and a user who recolours
|
||||
* their buttons should not thereby recolour their messages.
|
||||
* `--primary` is deliberately NOT one of these. "The color of a primary button" and
|
||||
* "the color that means informational" are different jobs, and a user who recolors
|
||||
* their buttons should not thereby recolor their messages.
|
||||
*/
|
||||
:root {
|
||||
--primary: var(--blue);
|
||||
|
|
@ -237,7 +237,7 @@ html[data-theme="amber"] {
|
|||
* Base element styling.
|
||||
*
|
||||
* semantic.min.css used to supply all of this, and removing it took the lot with it --
|
||||
* links lost their colour and gained the browser's underline and visited purple, and
|
||||
* links lost their color and gained the browser's underline and visited purple, and
|
||||
* every padded element silently changed size when `box-sizing: border-box` went away.
|
||||
* None of it belonged to Semantic; it belongs here, expressed in tokens.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue