@lifecorner/angular-ui-kit 3.27.0 → 3.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/lifecorner-angular-ui-kit-callout.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-callout.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-chat.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-chat.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-datepicker.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-document-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-gallery.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-kanban-board.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs +4 -4
- package/fesm2022/lifecorner-angular-ui-kit-log-viewer.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-notification-center.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-rich-text-editor.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-select.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-select.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs +4 -4
- package/fesm2022/lifecorner-angular-ui-kit-table.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-tag-input.mjs.map +1 -1
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs +2 -2
- package/fesm2022/lifecorner-angular-ui-kit-textarea.mjs.map +1 -1
- package/package.json +1 -1
- package/src/styles/_theme-dark.scss +154 -137
- package/src/styles/_theme-light.scss +158 -138
- package/src/styles/focus-indicators.spec.ts +14 -8
- package/src/styles/theme-contrast.spec.ts +14 -2
- package/src/styles/theme-modes.spec.ts +40 -0
- package/src/styles/touch-targets-contrast.spec.ts +286 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { compile } from 'sass';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Request 043 (3.27.1, design 043 D-1 to D-10): the follow-ups F-22 to F-27 of
|
|
6
|
+
* the 042 sweep, read from the compiled SCSS and the theme files. Hit areas
|
|
7
|
+
* grow without paint and the visible boxes keep their size (D-1, D-3); the
|
|
8
|
+
* dense toolbars get real 44 px buttons (D-2); field hover keeps 3:1 and never
|
|
9
|
+
* hides the error state (D-5); the rich-text frame is a field boundary (D-6);
|
|
10
|
+
* the F-26 inks and the kanban tint reach their contrast (D-8, D-10).
|
|
11
|
+
*/
|
|
12
|
+
const sheets = new Map<string, string>();
|
|
13
|
+
const css = (file: string): string => {
|
|
14
|
+
if (!sheets.has(file)) sheets.set(file, compile(resolve(__dirname, file.startsWith('_') ? file : `../lib/${file}`)).css);
|
|
15
|
+
return sheets.get(file) as string;
|
|
16
|
+
};
|
|
17
|
+
const forcedBlock = /@media \(forced-colors: active\) \{((?:[^{}]*\{[^{}]*\})*)\s*\}/g;
|
|
18
|
+
const rulesOf = (text: string) =>
|
|
19
|
+
[...text.replace(forcedBlock, '').matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({
|
|
20
|
+
selectors: m[1].split(/,(?![^(]*\))/).map((s) => s.trim()),
|
|
21
|
+
body: m[2],
|
|
22
|
+
}));
|
|
23
|
+
const decls = (body: string): Record<string, string> =>
|
|
24
|
+
Object.fromEntries([...body.matchAll(/([\w-]+):\s*([^;]+);/g)].map((m) => [m[1], m[2].replace(/\s+/g, ' ').trim()]));
|
|
25
|
+
/** Every rule naming `selector`, merged in source order, forced-colours blocks left out. */
|
|
26
|
+
const fills = (file: string, selector: string): Record<string, string> => {
|
|
27
|
+
const found = rulesOf(css(file)).filter((r) => r.selectors.includes(selector));
|
|
28
|
+
expect([file, selector, found.length > 0]).toEqual([file, selector, true]);
|
|
29
|
+
return Object.assign({}, ...found.map((r) => decls(r.body)));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ── Colours: the root rule of each theme file, var() followed ─────────────
|
|
33
|
+
type Rgba = [number, number, number, number];
|
|
34
|
+
function rootTokens(file: string, selector: string): Record<string, string> {
|
|
35
|
+
const text = css(file);
|
|
36
|
+
const open = text.indexOf('{', text.indexOf(selector));
|
|
37
|
+
const tokens: Record<string, string> = {};
|
|
38
|
+
for (const m of text.slice(open, text.indexOf('}', open)).matchAll(/(--[\w-]+):\s*([^;]+);/g)) tokens[m[1]] = m[2].replace(/\s+/g, ' ').trim();
|
|
39
|
+
return tokens;
|
|
40
|
+
}
|
|
41
|
+
const themes: [string, Record<string, string>][] = [
|
|
42
|
+
['light', rootTokens('_theme-light.scss', ':root,')],
|
|
43
|
+
['dark', rootTokens('_theme-dark.scss', ':root.dark,')],
|
|
44
|
+
];
|
|
45
|
+
function parse(value: string): Rgba {
|
|
46
|
+
const rgba = value.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
|
|
47
|
+
if (rgba) return [Number(rgba[1]), Number(rgba[2]), Number(rgba[3]), rgba[4] === undefined ? 1 : Number(rgba[4])];
|
|
48
|
+
if (!/^#[0-9a-f]{6}$/i.test(value)) throw new Error(`not a colour: ${value}`);
|
|
49
|
+
return [1, 3, 5].map((i) => parseInt(value.slice(i, i + 2), 16)).concat(1) as Rgba;
|
|
50
|
+
}
|
|
51
|
+
/** A declared value (`var(--x)`, `var(--x, fallback)`, hex, rgba) in one theme. */
|
|
52
|
+
function colour(tokens: Record<string, string>, value: string, depth = 0): Rgba {
|
|
53
|
+
const ref = value.trim().match(/^var\((--[\w-]+)(?:,\s*(.+))?\)$/);
|
|
54
|
+
if (!ref) return parse(value.trim());
|
|
55
|
+
if (depth > 10) throw new Error(`${value} does not resolve`);
|
|
56
|
+
return tokens[ref[1]] !== undefined ? colour(tokens, tokens[ref[1]], depth + 1) : colour(tokens, ref[2], depth + 1);
|
|
57
|
+
}
|
|
58
|
+
const over = (top: Rgba, base: Rgba): Rgba => [0, 1, 2].map((i) => top[i] * top[3] + base[i] * (1 - top[3])).concat(1) as Rgba;
|
|
59
|
+
const mix = (a: Rgba, share: number, b: Rgba): Rgba => [0, 1, 2].map((i) => a[i] * share + b[i] * (1 - share)).concat(1) as Rgba;
|
|
60
|
+
const lum = (c: Rgba) =>
|
|
61
|
+
c
|
|
62
|
+
.slice(0, 3)
|
|
63
|
+
.map((v) => v / 255)
|
|
64
|
+
.map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4))
|
|
65
|
+
.reduce((sum, v, i) => sum + v * [0.2126, 0.7152, 0.0722][i], 0);
|
|
66
|
+
/** WCAG ratio, floored to two decimals (as the design's measure script). */
|
|
67
|
+
const ratio = (a: Rgba, b: Rgba) => {
|
|
68
|
+
const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x);
|
|
69
|
+
return Math.floor(((hi + 0.05) / (lo + 0.05)) * 100) / 100;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const TOUCH = 'var(--size-min-touch-height, 2.75rem)';
|
|
73
|
+
|
|
74
|
+
describe('Touch targets, hover borders and contrast (request 043)', () => {
|
|
75
|
+
it('the touch size token is 44 px in both themes', () => {
|
|
76
|
+
for (const [, t] of themes) expect(t['--size-min-touch-height']).toBe('2.75rem');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('D-1 / D-3: hit areas without paint; the visible control keeps its size (AC-1, AC-2)', () => {
|
|
80
|
+
const centred = { 'inset-inline': `min(0px, (100% - ${TOUCH}) / 2)`, top: `calc(50% - ${TOUCH} / 2)`, bottom: `calc(50% - ${TOUCH} / 2)` };
|
|
81
|
+
// [sheet, control, its hit area, the visible box that must not change]
|
|
82
|
+
const areas: [string, string, Record<string, string>, Record<string, string>][] = [
|
|
83
|
+
['notification-center/notification-center.component.scss', '.lc-nc__action-btn', centred, { padding: '4px 8px', 'font-size': '0.75rem' }],
|
|
84
|
+
['notification-center/notification-center.component.scss', '.lc-nc__filter', centred, { padding: '4px 10px', 'font-size': '0.6875rem', 'white-space': 'nowrap' }],
|
|
85
|
+
[
|
|
86
|
+
'log-viewer/log-viewer.component.scss',
|
|
87
|
+
'.lc-log-viewer__level-pill',
|
|
88
|
+
{ 'inset-inline': `min(-0.125rem, (100% - ${TOUCH}) / 2)`, top: centred.top, bottom: centred.bottom },
|
|
89
|
+
{ padding: '0.125rem 0.5rem', 'font-size': '0.6875rem' },
|
|
90
|
+
],
|
|
91
|
+
['callout/callout.component.scss', '.callout__dismiss', { inset: `calc((1.5rem - ${TOUCH}) / 2)` }, { width: '1.5rem', height: '1.5rem' }],
|
|
92
|
+
['chat/chat.component.scss', '.lc-chat__send-btn', { inset: `calc((2rem - ${TOUCH}) / 2)` }, { width: '2rem', height: '2rem' }],
|
|
93
|
+
['chat/chat.component.scss', '.lc-chat__attach-btn', { inset: `calc((2rem - ${TOUCH}) / 2)` }, { width: '2rem', height: '2rem' }],
|
|
94
|
+
// D-3: 24 × 24 (WCAG 2.5.8 by size): 44 would cover the next chip row.
|
|
95
|
+
['tag-input/tag-input.component.scss', '.lc-tag-input__tag-remove', { inset: '-0.25rem' }, { width: '16px', height: '16px' }],
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
it.each(areas)('%s %s: a positioned host and a transparent ::before with exactly the hit-area box', (file, cls, area, box) => {
|
|
99
|
+
expect(fills(file, cls).position).toBe('relative');
|
|
100
|
+
expect(fills(file, `${cls}::before`)).toEqual({ content: '""', position: 'absolute', ...area });
|
|
101
|
+
const host = fills(file, cls);
|
|
102
|
+
for (const [property, value] of Object.entries(box)) expect([cls, property, host[property]]).toEqual([cls, property, value]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Review 043 r1 F3: the select × was a 20 px box with a 24 px `::before`; axe
|
|
106
|
+
// `target-size` reads the element's own box and flagged it (×12). It is now a
|
|
107
|
+
// 24 px box whose 2 px padding the negative margin takes back: the layout
|
|
108
|
+
// footprint, the 20 px hover fill (content-box) and the ring (offset 0 on 24 px
|
|
109
|
+
// = 2 px round 20 px) are unchanged; 44 px would leave the field.
|
|
110
|
+
it('the select × is a 24 × 24 px box with the 20 px layout, hover fill and ring of 3.27.0', () => {
|
|
111
|
+
const file = 'select/select.component.scss';
|
|
112
|
+
expect(fills(file, '.lc-select__clear')).toMatchObject({
|
|
113
|
+
width: '1.5rem',
|
|
114
|
+
height: '1.5rem',
|
|
115
|
+
margin: '-0.125rem',
|
|
116
|
+
padding: '0.125rem',
|
|
117
|
+
'background-clip': 'content-box',
|
|
118
|
+
'border-radius': 'calc(var(--border-radius-sm) + 0.125rem)',
|
|
119
|
+
});
|
|
120
|
+
expect(rulesOf(css(file)).some((r) => r.selectors.includes('.lc-select__clear::before'))).toBe(false);
|
|
121
|
+
expect(fills(file, '.lc-select__clear:focus-visible')['outline-offset']).toBe('0');
|
|
122
|
+
expect(fills(file, '.lc-select__clear:hover')['background-clip']).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('the 24 px areas are 24 px: 1.5rem with 24 − 2 × 2 = 20 in the layout (select ×) and 16 + 2 × 4 (tag ×)', () => {
|
|
126
|
+
expect(1.5 * 16).toBe(24);
|
|
127
|
+
expect(1.5 * 16 - 2 * 0.125 * 16).toBe(20);
|
|
128
|
+
expect(16 + 2 * 0.25 * 16).toBe(24);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('the rows the hit areas sit in grew just enough: NC filters 9 px (26.5 + 2 × 8.75), log toolbar 8 px', () => {
|
|
132
|
+
expect(fills('notification-center/notification-center.component.scss', '.lc-nc__filters').padding).toBe('9px 16px');
|
|
133
|
+
expect(fills('log-viewer/log-viewer.component.scss', '.lc-log-viewer__toolbar').padding).toBe('0.5rem');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('D-2: real 44 px buttons where the pitch leaves no room (AC-1, AC-2)', () => {
|
|
138
|
+
it('rich-text toolbar buttons are 44 × 44, mode buttons at least 44 × 44', () => {
|
|
139
|
+
const btn = fills('rich-text-editor/rich-text-editor.component.scss', '.lc-rich-text-editor__toolbar-btn');
|
|
140
|
+
expect([btn.width, btn.height]).toEqual([TOUCH, TOUCH]);
|
|
141
|
+
const mode = fills('rich-text-editor/rich-text-editor.component.scss', '.lc-rich-text-editor__mode-btn');
|
|
142
|
+
expect([mode['min-width'], mode['min-height'], mode.padding]).toEqual([TOUCH, TOUCH, 'var(--spacing-1) var(--spacing-2)']);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('document-viewer tools are 44 × 44 and the toolbar wraps instead of overflowing', () => {
|
|
146
|
+
const file = 'document-viewer/document-viewer.component.scss';
|
|
147
|
+
const btn = fills(file, '.doc-viewer__btn');
|
|
148
|
+
expect([btn.width, btn.height]).toEqual([TOUCH, TOUCH]);
|
|
149
|
+
expect(fills(file, '.doc-viewer__toolbar')['flex-wrap']).toBe('wrap');
|
|
150
|
+
expect(fills(file, '.doc-viewer__toolbar-left')).toMatchObject({ flex: '1 1 10rem', 'min-width': '0' });
|
|
151
|
+
expect(fills(file, '.doc-viewer__toolbar-right')).toMatchObject({
|
|
152
|
+
'flex-wrap': 'wrap',
|
|
153
|
+
'justify-content': 'flex-end',
|
|
154
|
+
'row-gap': 'var(--spacing-1)',
|
|
155
|
+
'flex-shrink': '1',
|
|
156
|
+
'min-width': '0',
|
|
157
|
+
'margin-inline-start': 'auto',
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('D-5: field hover (F-23, AC-3, AC-4)', () => {
|
|
163
|
+
// [sheet, the field's selector prefix, its error class]
|
|
164
|
+
const fields: [string, RegExp, string][] = [
|
|
165
|
+
['input/input.component.scss', /^\.input-container[:.]/, '.input-invalid'],
|
|
166
|
+
['select/select.component.scss', /^\.lc-select[:.]/, '.lc-select--error'],
|
|
167
|
+
['textarea/textarea.component.scss', /^\.lc-textarea--(outline|filled)[:.]/, '.lc-textarea--error'],
|
|
168
|
+
['datepicker/datepicker.component.scss', /^\.lc-datepicker[:.]/, '.lc-datepicker--error'],
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
it.each(fields)('AC-4: %s: every hover rule on the field skips the error state', (file, field, error) => {
|
|
172
|
+
const hovers = rulesOf(css(file))
|
|
173
|
+
.flatMap((r) => r.selectors)
|
|
174
|
+
.filter((s) => field.test(s) && s.includes(':hover'));
|
|
175
|
+
expect(hovers.length).toBeGreaterThan(0);
|
|
176
|
+
for (const s of hovers) expect([s, s.includes(`:not(${error})`)]).toEqual([s, true]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it.each(fields)('AC-3: %s: no hover border below 3:1 (primary-400 is gone)', (file, field) => {
|
|
180
|
+
const borders = rulesOf(css(file))
|
|
181
|
+
.filter((r) => r.selectors.some((s) => field.test(s) && s.includes(':hover')))
|
|
182
|
+
.map((r) => decls(r.body)['border-color'])
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
expect(borders.every((b) => b === 'var(--color-text-secondary)' || b === 'var(--color-border)')).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('AC-3: the outline hover border (--color-text-secondary) is ≥ 3:1 on the page, the surface and surface-2', () => {
|
|
188
|
+
const got = themes.map(([, t]) =>
|
|
189
|
+
['--color-background', '--color-surface', '--color-surface-2'].map((bg) => ratio(colour(t, 'var(--color-text-secondary)'), colour(t, `var(${bg})`))),
|
|
190
|
+
);
|
|
191
|
+
expect(got).toEqual([
|
|
192
|
+
[7.55, 7.55, 7.23],
|
|
193
|
+
[12.16, 11.15, 9.25],
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('AC-4: the error border at rest (and so under the pointer) is ≥ 3:1: 8.39 light, 9.47 dark on the page', () => {
|
|
198
|
+
expect(themes.map(([, t]) => ratio(colour(t, 'var(--color-error-600)'), colour(t, 'var(--color-background)')))).toEqual([8.39, 9.47]);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('D-6: the rich-text frame is a field boundary (F-24, AC-5)', () => {
|
|
203
|
+
it('uses lc-input’s --color-field-border: 4.83 light, 7.31 / 6.70 dark (page / surface)', () => {
|
|
204
|
+
expect(fills('rich-text-editor/rich-text-editor.component.scss', '.lc-rich-text-editor').border).toBe('1px solid var(--color-field-border)');
|
|
205
|
+
expect(fills('input/input.component.scss', '.input-container').border).toBe('1px solid var(--color-field-border)');
|
|
206
|
+
const got = themes.map(([, t]) =>
|
|
207
|
+
['--color-background', '--color-surface'].map((bg) => ratio(colour(t, 'var(--color-field-border)'), colour(t, `var(${bg})`))),
|
|
208
|
+
);
|
|
209
|
+
expect(got).toEqual([
|
|
210
|
+
[4.83, 4.83],
|
|
211
|
+
[7.31, 6.7],
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('D-8: inks (F-26, AC-8)', () => {
|
|
217
|
+
it('RTE mode .active and the gallery --active chip use the fill pair: 6.21:1 in both themes', () => {
|
|
218
|
+
expect(fills('rich-text-editor/rich-text-editor.component.scss', '.lc-rich-text-editor__mode-btn.active')).toEqual({
|
|
219
|
+
'background-color': 'var(--color-primary-fill)',
|
|
220
|
+
color: 'var(--color-on-primary)',
|
|
221
|
+
});
|
|
222
|
+
expect(fills('gallery/gallery.component.scss', '.gallery__filter-btn--active')).toEqual({
|
|
223
|
+
background: 'var(--color-primary-fill)',
|
|
224
|
+
'border-color': 'var(--color-primary-500)',
|
|
225
|
+
color: 'var(--color-on-primary)',
|
|
226
|
+
});
|
|
227
|
+
expect(themes.map(([, t]) => ratio(colour(t, 'var(--color-on-primary)'), colour(t, 'var(--color-primary-fill)')))).toEqual([6.21, 6.21]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('gallery hover: the active chip darkens to the fill hover (9.13:1), an inactive chip takes the link ink', () => {
|
|
231
|
+
const file = 'gallery/gallery.component.scss';
|
|
232
|
+
expect(fills(file, '.gallery__filter-btn--active:hover')).toMatchObject({ background: 'var(--color-primary-fill-hover)', color: 'var(--color-on-primary)' });
|
|
233
|
+
expect(fills(file, '.gallery__filter-btn:hover').color).toBe('var(--color-link)');
|
|
234
|
+
expect(themes.map(([, t]) => ratio(colour(t, 'var(--color-on-primary)'), colour(t, 'var(--color-primary-fill-hover)')))).toEqual([9.13, 9.13]);
|
|
235
|
+
expect(themes.map(([, t]) => ratio(colour(t, 'var(--color-link)'), colour(t, 'var(--color-background)')))).toEqual([6.21, 11.78]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('tag chip text on its tint over surface-2: 7.18 light, 6.34 dark (the dark value is today’s)', () => {
|
|
239
|
+
const chip = fills('tag-input/tag-input.component.scss', '.lc-tag-input__tag');
|
|
240
|
+
expect(chip.color).toBe('var(--color-on-primary-subtle)');
|
|
241
|
+
const got = themes.map(([, t]) => {
|
|
242
|
+
const bg = over(parse(chip.background), colour(t, 'var(--color-surface-2)'));
|
|
243
|
+
return ratio(colour(t, chip.color), bg);
|
|
244
|
+
});
|
|
245
|
+
expect(got).toEqual([7.18, 6.34]);
|
|
246
|
+
expect(colour(themes[1][1], 'var(--color-on-primary-subtle)')).toEqual(colour(themes[1][1], 'var(--color-primary-500)'));
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('chat monogram on its 18 % tint: 7.00 light, 5.11 dark (unchanged)', () => {
|
|
250
|
+
const mono = fills('chat/chat.component.scss', '.lc-chat__monogram');
|
|
251
|
+
expect([mono.color, mono.background]).toEqual([
|
|
252
|
+
'var(--color-on-primary-subtle)',
|
|
253
|
+
'color-mix(in srgb, var(--color-primary-500) 18%, var(--color-surface-2))',
|
|
254
|
+
]);
|
|
255
|
+
const got = themes.map(([, t]) => ratio(colour(t, mono.color), mix(colour(t, 'var(--color-primary-500)'), 0.18, colour(t, 'var(--color-surface-2)'))));
|
|
256
|
+
expect(got).toEqual([7, 5.11]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('no raw white is left in the changed rules', () => {
|
|
260
|
+
for (const [file, selector] of [
|
|
261
|
+
['rich-text-editor/rich-text-editor.component.scss', '.lc-rich-text-editor__mode-btn.active'],
|
|
262
|
+
['gallery/gallery.component.scss', '.gallery__filter-btn--active'],
|
|
263
|
+
['gallery/gallery.component.scss', '.gallery__filter-btn--active:hover'],
|
|
264
|
+
]) {
|
|
265
|
+
expect(Object.values(fills(file, selector)).join(' ')).not.toMatch(/#fff|\bwhite\b/i);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('D-10: the over-limit kanban column (F-27, AC-10)', () => {
|
|
271
|
+
it('is --color-error-subtle: the title (text-secondary) reads 6.58 light, ≥ 4.5 dark on the page and the surface', () => {
|
|
272
|
+
const file = 'kanban-board/kanban-board.component.scss';
|
|
273
|
+
expect(fills(file, '.lc-kanban__column--over-limit').background).toBe('var(--color-error-subtle)');
|
|
274
|
+
expect(fills(file, '.lc-kanban__column-header').color).toBe('var(--color-text-secondary)');
|
|
275
|
+
const got = themes.map(([, t]) =>
|
|
276
|
+
['--color-background', '--color-surface'].map((bg) => ratio(colour(t, 'var(--color-text-secondary)'), over(colour(t, 'var(--color-error-subtle)'), colour(t, `var(${bg})`)))),
|
|
277
|
+
);
|
|
278
|
+
expect(got[0]).toEqual([6.58, 6.58]);
|
|
279
|
+
expect(Math.min(...got[1])).toBeGreaterThanOrEqual(4.5);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('the inset card ring sits on the card surface: 4.36 light, 9.12 dark', () => {
|
|
283
|
+
expect(themes.map(([, t]) => ratio(colour(t, 'var(--color-primary-500)'), colour(t, 'var(--color-surface)')))).toEqual([4.36, 9.12]);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|