@electrovir/color 0.0.0 → 1.1.0
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/dist/{color-formats.d.ts → color-class/color-formats.d.ts} +28 -22
- package/dist/{color-formats.js → color-class/color-formats.js} +24 -10
- package/dist/{color.d.ts → color-class/color.d.ts} +6 -6
- package/dist/{color.js → color-class/color.js} +13 -12
- package/dist/contrast/contrast.d.ts +229 -0
- package/dist/contrast/contrast.js +254 -0
- package/dist/elements/vir-all-color-space-sliders.element.d.ts +11 -0
- package/dist/elements/vir-all-color-space-sliders.element.js +52 -0
- package/dist/elements/vir-color-format-sliders.element.d.ts +13 -0
- package/dist/elements/vir-color-format-sliders.element.js +54 -0
- package/dist/elements/vir-color-pair.element.d.ts +23 -0
- package/dist/elements/vir-color-pair.element.js +196 -0
- package/dist/elements/vir-color-slider.element.d.ts +14 -0
- package/dist/elements/vir-color-slider.element.js +101 -0
- package/dist/elements/vir-contrast-indicator.element.d.ts +10 -0
- package/dist/elements/vir-contrast-indicator.element.js +96 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +9 -3
- package/package.json +48 -37
- /package/dist/{color-name-length.d.ts → color-class/color-name-length.d.ts} +0 -0
- /package/dist/{color-name-length.js → color-class/color-name-length.js} +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
+
import { arrayToObject, mapObjectValues, round, } from '@augment-vir/common';
|
|
3
|
+
// @ts-expect-error: `fontLookupAPCA` is not in the types
|
|
4
|
+
import { calcAPCA, fontLookupAPCA } from 'apca-w3';
|
|
5
|
+
/**
|
|
6
|
+
* All considered font weights in {@link FontWeight} mapped by their weight name.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
*/
|
|
10
|
+
export const fontWeightByName = {
|
|
11
|
+
Thin: 100,
|
|
12
|
+
ExtraLight: 200,
|
|
13
|
+
Light: 300,
|
|
14
|
+
Normal: 400,
|
|
15
|
+
Medium: 500,
|
|
16
|
+
SemiBold: 600,
|
|
17
|
+
Bold: 700,
|
|
18
|
+
ExtraBold: 800,
|
|
19
|
+
Heavy: 900,
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* All font weight names from {@link fontWeightByName}.
|
|
23
|
+
*
|
|
24
|
+
* @category Internal
|
|
25
|
+
*/
|
|
26
|
+
export const FontWeightName = mapObjectValues(fontWeightByName, (key) => key);
|
|
27
|
+
/**
|
|
28
|
+
* All considered font weights in {@link FontWeight} mapped to their weight name from
|
|
29
|
+
* {@link fontWeightByName}.
|
|
30
|
+
*
|
|
31
|
+
* @category Internal
|
|
32
|
+
*/
|
|
33
|
+
export const fontWeightToName = Object.fromEntries(Object.entries(fontWeightByName).map(([key, value,]) => [
|
|
34
|
+
value,
|
|
35
|
+
key,
|
|
36
|
+
]));
|
|
37
|
+
/**
|
|
38
|
+
* Calculate contrast for the given color combination.
|
|
39
|
+
*
|
|
40
|
+
* @category Internal
|
|
41
|
+
*/
|
|
42
|
+
export function calculateContrast({ background, foreground, }) {
|
|
43
|
+
const contrast = round(Number(calcAPCA(foreground, background)), { digits: 1 });
|
|
44
|
+
return {
|
|
45
|
+
contrast,
|
|
46
|
+
fontSizes: calculateFontSizes(contrast),
|
|
47
|
+
contrastLevel: determineContrastLevel(contrast),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** @category Internal */
|
|
51
|
+
export function findClosestColor(baseColor, possibleColors) {
|
|
52
|
+
return possibleColors.reduce((best, color) => {
|
|
53
|
+
const contrast = Math.abs(calculateContrast({
|
|
54
|
+
foreground: color,
|
|
55
|
+
background: baseColor,
|
|
56
|
+
}).contrast);
|
|
57
|
+
if (contrast > best.contrast) {
|
|
58
|
+
return best;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return {
|
|
62
|
+
contrast,
|
|
63
|
+
color,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}, {
|
|
67
|
+
contrast: Infinity,
|
|
68
|
+
color: '',
|
|
69
|
+
}).color;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Find a color from an array that matches the desired contrast level.
|
|
73
|
+
*
|
|
74
|
+
* @category Internal
|
|
75
|
+
* @returns `undefined` if no color match is found.
|
|
76
|
+
*/
|
|
77
|
+
export function findColorAtContrastLevel(colors, desiredContrastLevel) {
|
|
78
|
+
const otherColors = check.isArray(colors.foreground)
|
|
79
|
+
? colors.foreground
|
|
80
|
+
: check.isArray(colors.background)
|
|
81
|
+
? colors.background
|
|
82
|
+
: new Error('No color array provided.');
|
|
83
|
+
if (otherColors instanceof Error) {
|
|
84
|
+
throw otherColors;
|
|
85
|
+
}
|
|
86
|
+
const desiredIndex = orderedContrastLevelNames.indexOf(desiredContrastLevel);
|
|
87
|
+
const bestMatch = otherColors.reduce((best, otherColor) => {
|
|
88
|
+
const contrast = calculateContrast({
|
|
89
|
+
foreground: check.isString(colors.foreground) ? colors.foreground : otherColor,
|
|
90
|
+
background: check.isString(colors.background) ? colors.background : otherColor,
|
|
91
|
+
});
|
|
92
|
+
const contrastIndex = orderedContrastLevelNames.indexOf(contrast.contrastLevel.name);
|
|
93
|
+
const distance = contrastIndex - desiredIndex;
|
|
94
|
+
if (distance > 0 || best.distance > distance) {
|
|
95
|
+
return best;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
return {
|
|
99
|
+
color: otherColor,
|
|
100
|
+
distance,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}, {
|
|
104
|
+
distance: 0,
|
|
105
|
+
color: undefined,
|
|
106
|
+
});
|
|
107
|
+
return bestMatch.color;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Calculated needed font sizes for each font weight for the given color contrast.
|
|
111
|
+
*
|
|
112
|
+
* @category Internal
|
|
113
|
+
*/
|
|
114
|
+
export function calculateFontSizes(contrast) {
|
|
115
|
+
const fontLookup = fontLookupAPCA(contrast).slice(1);
|
|
116
|
+
const sizes = arrayToObject(fontLookup, (value, index) => {
|
|
117
|
+
return {
|
|
118
|
+
key: (index + 1) * 100,
|
|
119
|
+
value,
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
return sizes;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Finds the color contrast level for the given contrast.
|
|
126
|
+
*
|
|
127
|
+
* @category Internal
|
|
128
|
+
*/
|
|
129
|
+
export function determineContrastLevel(contrast) {
|
|
130
|
+
return assertWrap.isDefined(contrastLevels.find((threshold) => threshold.min <= Math.abs(contrast)));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Names for each {@link ContrastLevel}.
|
|
134
|
+
*
|
|
135
|
+
* @category Internal
|
|
136
|
+
*/
|
|
137
|
+
export var ContrastLevelName;
|
|
138
|
+
(function (ContrastLevelName) {
|
|
139
|
+
ContrastLevelName["SmallBodyText"] = "small-body";
|
|
140
|
+
ContrastLevelName["BodyText"] = "body";
|
|
141
|
+
ContrastLevelName["NonBodyText"] = "non-body";
|
|
142
|
+
ContrastLevelName["Header"] = "header";
|
|
143
|
+
ContrastLevelName["Placeholder"] = "placeholder";
|
|
144
|
+
ContrastLevelName["Decoration"] = "decoration";
|
|
145
|
+
ContrastLevelName["Invisible"] = "invisible";
|
|
146
|
+
})(ContrastLevelName || (ContrastLevelName = {}));
|
|
147
|
+
/**
|
|
148
|
+
* User-facing labels for {@link ContrastLevelName}.
|
|
149
|
+
*
|
|
150
|
+
* @category Internal
|
|
151
|
+
*/
|
|
152
|
+
export const contrastLevelLabel = {
|
|
153
|
+
[ContrastLevelName.SmallBodyText]: 'Small Text',
|
|
154
|
+
[ContrastLevelName.BodyText]: 'Body Text',
|
|
155
|
+
[ContrastLevelName.NonBodyText]: 'Non-body Text',
|
|
156
|
+
[ContrastLevelName.Header]: 'Header',
|
|
157
|
+
[ContrastLevelName.Placeholder]: 'Placeholder',
|
|
158
|
+
[ContrastLevelName.Decoration]: 'Decoration',
|
|
159
|
+
[ContrastLevelName.Invisible]: 'Invisible',
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* All {@link ContrastLevelName} values in order from highest contrast to lowest.
|
|
163
|
+
*
|
|
164
|
+
* @category Internal
|
|
165
|
+
*/
|
|
166
|
+
export const orderedContrastLevelNames = [
|
|
167
|
+
ContrastLevelName.SmallBodyText,
|
|
168
|
+
ContrastLevelName.BodyText,
|
|
169
|
+
ContrastLevelName.NonBodyText,
|
|
170
|
+
ContrastLevelName.Header,
|
|
171
|
+
ContrastLevelName.Placeholder,
|
|
172
|
+
ContrastLevelName.Decoration,
|
|
173
|
+
ContrastLevelName.Invisible,
|
|
174
|
+
];
|
|
175
|
+
/**
|
|
176
|
+
* All color contrast levels corresponding to APCA bronze guidelines.
|
|
177
|
+
*
|
|
178
|
+
* @category Internal
|
|
179
|
+
*/
|
|
180
|
+
export const contrastLevels = [
|
|
181
|
+
{
|
|
182
|
+
min: 90,
|
|
183
|
+
name: ContrastLevelName.SmallBodyText,
|
|
184
|
+
description: 'Perfect for all sizes of text, even small body text.',
|
|
185
|
+
apcaName: 'small body text only',
|
|
186
|
+
apcaDescription: 'Preferred level for fluent text and columns of body text with a font no smaller than 18px/weight 300 or 14px/weight 400 (normal), or non-body text with a font no smaller than 12px. Also a recommended minimum for extremely thin fonts with a minimum of 24px at weight 200. Lc 90 is a suggested maximum for very large and bold fonts (greater than 36px bold), and large areas of color.',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
min: 75,
|
|
190
|
+
name: ContrastLevelName.BodyText,
|
|
191
|
+
description: 'Good for regular body text and anything larger.',
|
|
192
|
+
apcaName: 'body text okay',
|
|
193
|
+
apcaDescription: 'The minimum level for columns of body text with a font no smaller than 24px/300 weight, 18px/400, 16px/500 and 14px/700. This level may be used with non-body text with a font no smaller than 15px/400. Also, Lc 75 should be considered a minimum for larger for any larger text where readability is important.',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
min: 60,
|
|
197
|
+
name: ContrastLevelName.NonBodyText,
|
|
198
|
+
description: 'Good for legible non-body text and anything larger.',
|
|
199
|
+
apcaName: 'fluent text only',
|
|
200
|
+
apcaDescription: 'The minimum level recommended for content text that is not body, column, or block text. In other words, text you want people to read. The minimums: no smaller than 48px/200, 36px/300, 24px normal weight (400), 21px/500, 18px/600, 16px/700 (bold). These values based on the reference font Helvetica. To use these sizes as body text, add Lc 15 to the minimum contrast.',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
min: 45,
|
|
204
|
+
name: ContrastLevelName.Header,
|
|
205
|
+
description: 'Okay for large or headline text.',
|
|
206
|
+
apcaName: 'large & sub-fluent text',
|
|
207
|
+
apcaDescription: 'The minimum for larger, heavier text (36px normal weight or 24px bold) such as headlines, and large text that should be fluently readable but is not body text. This is also the minimum for pictograms with fine details, or smaller outline icons, , no less than 4px in its smallest dimension.',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
min: 30,
|
|
211
|
+
name: ContrastLevelName.Placeholder,
|
|
212
|
+
description: 'Okay for disabled or placeholder text, copyright lines, icons, or non-text elements.',
|
|
213
|
+
apcaName: 'spot & non text only',
|
|
214
|
+
apcaDescription: 'The absolute minimum for any text not listed above, which means non-content text considered as "spot readable". This includes placeholder text and disabled element text, and some non-content like a copyright bug. This is also the minimum for large/solid semantic & understandable non-text elements such as "mostly solid" icons or pictograms, no less than 10px in its smallest dimension.',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
min: 15,
|
|
218
|
+
name: ContrastLevelName.Decoration,
|
|
219
|
+
description: 'Only okay for decorations like graphics, borders, dividers, etc. Do not use for any text.',
|
|
220
|
+
apcaName: 'no text usage',
|
|
221
|
+
apcaDescription: 'The absolute minimum for any non-text that needs to be discernible and differentiable, but does not apply to semantic non-text such as icons, and is no less than 15px in its smallest dimension. This may include dividers, and in some cases large buttons or thick focus visible outlines, but does not include fine details which have a higher minimum. Designers should treat anything below this level as invisible, as it will not be visible for many users. This minimum level should be avoided for any items important to the use, understanding, or interaction of the site.',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
min: 0,
|
|
225
|
+
name: ContrastLevelName.Invisible,
|
|
226
|
+
description: 'Effectively invisible for users.',
|
|
227
|
+
apcaName: 'invisible',
|
|
228
|
+
apcaDescription: 'This should be treated as invisible.',
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
/**
|
|
232
|
+
* A mapping of all color contrast levels mins to their contrast levels. Generated from
|
|
233
|
+
* {@link contrastLevels}.
|
|
234
|
+
*
|
|
235
|
+
* @category Internal
|
|
236
|
+
*/
|
|
237
|
+
export const contrastLevelMinMap = arrayToObject(contrastLevels, (contrastLevel) => {
|
|
238
|
+
return {
|
|
239
|
+
key: contrastLevel.min,
|
|
240
|
+
value: contrastLevel,
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
/**
|
|
244
|
+
* A mapping of all color contrast levels mins to their contrast levels. Generated from
|
|
245
|
+
* {@link contrastLevels}.
|
|
246
|
+
*
|
|
247
|
+
* @category Internal
|
|
248
|
+
*/
|
|
249
|
+
export const contrastLevelNameMap = arrayToObject(contrastLevels, (contrastLevel) => {
|
|
250
|
+
return {
|
|
251
|
+
key: contrastLevel.name,
|
|
252
|
+
value: contrastLevel,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Color } from '../color-class/color.js';
|
|
2
|
+
/**
|
|
3
|
+
* Color sliders for all color spaces.
|
|
4
|
+
*
|
|
5
|
+
* @category Elements
|
|
6
|
+
*/
|
|
7
|
+
export declare const VirAllColorSpaceSliders: import("element-vir").DeclarativeElementDefinition<"vir-all-color-space-sliders", {
|
|
8
|
+
color: Readonly<Color>;
|
|
9
|
+
}, {}, {
|
|
10
|
+
colorChange: import("element-vir").DefineEvent<string>;
|
|
11
|
+
}, "vir-all-color-space-sliders-", "vir-all-color-space-sliders-", readonly [], readonly []>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* node:coverage disable */
|
|
2
|
+
import { getObjectTypedKeys, getObjectTypedValues } from '@augment-vir/common';
|
|
3
|
+
import { css, defineElement, defineElementEvent, html, listen } from 'element-vir';
|
|
4
|
+
import { colorFormatsBySpace } from '../color-class/color-formats.js';
|
|
5
|
+
import { VirColorFormatSliders } from './vir-color-format-sliders.element.js';
|
|
6
|
+
/**
|
|
7
|
+
* Color sliders for all color spaces.
|
|
8
|
+
*
|
|
9
|
+
* @category Elements
|
|
10
|
+
*/
|
|
11
|
+
export const VirAllColorSpaceSliders = defineElement()({
|
|
12
|
+
tagName: 'vir-all-color-space-sliders',
|
|
13
|
+
styles: css `
|
|
14
|
+
:host {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: 16px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.color-space {
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-wrap: wrap;
|
|
23
|
+
column-gap: 32px;
|
|
24
|
+
row-gap: 8px;
|
|
25
|
+
}
|
|
26
|
+
`,
|
|
27
|
+
events: {
|
|
28
|
+
colorChange: defineElementEvent(),
|
|
29
|
+
},
|
|
30
|
+
render({ inputs, dispatch, events }) {
|
|
31
|
+
const colorSpaceTemplates = getObjectTypedValues(colorFormatsBySpace).map((colorSpaceFormats) => {
|
|
32
|
+
const formatTemplates = getObjectTypedKeys(colorSpaceFormats).map((colorFormatName) => {
|
|
33
|
+
return html `
|
|
34
|
+
<${VirColorFormatSliders.assign({
|
|
35
|
+
color: inputs.color,
|
|
36
|
+
colorFormatName,
|
|
37
|
+
})}
|
|
38
|
+
${listen(VirColorFormatSliders.events.colorChange, (event) => {
|
|
39
|
+
dispatch(new events.colorChange(event.detail));
|
|
40
|
+
})}
|
|
41
|
+
></${VirColorFormatSliders}>
|
|
42
|
+
`;
|
|
43
|
+
});
|
|
44
|
+
return html `
|
|
45
|
+
<section class="color-space">${formatTemplates}</section>
|
|
46
|
+
`;
|
|
47
|
+
});
|
|
48
|
+
return html `
|
|
49
|
+
${colorSpaceTemplates}
|
|
50
|
+
`;
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ColorFormatName } from '../color-class/color-formats.js';
|
|
2
|
+
import { type Color } from '../color-class/color.js';
|
|
3
|
+
/**
|
|
4
|
+
* Color sliders for all coordinates within a specific color format.
|
|
5
|
+
*
|
|
6
|
+
* @category Elements
|
|
7
|
+
*/
|
|
8
|
+
export declare const VirColorFormatSliders: import("element-vir").DeclarativeElementDefinition<"vir-color-format-sliders", {
|
|
9
|
+
color: Readonly<Color>;
|
|
10
|
+
colorFormatName: ColorFormatName;
|
|
11
|
+
}, {}, {
|
|
12
|
+
colorChange: import("element-vir").DefineEvent<string>;
|
|
13
|
+
}, "vir-color-format-sliders-", "vir-color-format-sliders-", readonly [], readonly []>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* node:coverage disable */
|
|
2
|
+
import { getObjectTypedKeys } from '@augment-vir/common';
|
|
3
|
+
import { css, defineElement, defineElementEvent, html, listen } from 'element-vir';
|
|
4
|
+
import { noNativeSpacing } from 'vira';
|
|
5
|
+
import { colorFormats, } from '../color-class/color-formats.js';
|
|
6
|
+
import { VirColorSlider } from './vir-color-slider.element.js';
|
|
7
|
+
/**
|
|
8
|
+
* Color sliders for all coordinates within a specific color format.
|
|
9
|
+
*
|
|
10
|
+
* @category Elements
|
|
11
|
+
*/
|
|
12
|
+
export const VirColorFormatSliders = defineElement()({
|
|
13
|
+
tagName: 'vir-color-format-sliders',
|
|
14
|
+
styles: css `
|
|
15
|
+
:host {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
h3 {
|
|
21
|
+
${noNativeSpacing};
|
|
22
|
+
}
|
|
23
|
+
`,
|
|
24
|
+
events: {
|
|
25
|
+
colorChange: defineElementEvent(),
|
|
26
|
+
},
|
|
27
|
+
render({ inputs, dispatch, events }) {
|
|
28
|
+
const colorFormat = colorFormats[inputs.colorFormatName];
|
|
29
|
+
const coordinateTemplates = getObjectTypedKeys(colorFormat.coords).map((colorCoordinate) => {
|
|
30
|
+
return html `
|
|
31
|
+
<${VirColorSlider.assign({
|
|
32
|
+
color: inputs.color,
|
|
33
|
+
colorCoordinateName: colorCoordinate,
|
|
34
|
+
colorFormatName: inputs.colorFormatName,
|
|
35
|
+
})}
|
|
36
|
+
${listen(VirColorSlider.events.valueChange, (event) => {
|
|
37
|
+
const newColor = inputs.color.clone();
|
|
38
|
+
newColor.set({
|
|
39
|
+
[inputs.colorFormatName]: {
|
|
40
|
+
[colorCoordinate]: event.detail,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const newValue = newColor.toCss()[inputs.colorFormatName];
|
|
44
|
+
dispatch(new events.colorChange(newValue));
|
|
45
|
+
})}
|
|
46
|
+
></${VirColorSlider}>
|
|
47
|
+
`;
|
|
48
|
+
});
|
|
49
|
+
return html `
|
|
50
|
+
<h3>${inputs.colorFormatName}</h3>
|
|
51
|
+
${coordinateTemplates}
|
|
52
|
+
`;
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type SingleCssVarDefinition } from 'lit-css-vars';
|
|
2
|
+
import { type FontWeight } from '../contrast/contrast.js';
|
|
3
|
+
/**
|
|
4
|
+
* A foreground/background color pair.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export type ColorPair = Record<'foreground' | 'background', SingleCssVarDefinition>;
|
|
9
|
+
/**
|
|
10
|
+
* Showcase a foreground/backend color pair.
|
|
11
|
+
*
|
|
12
|
+
* @category Elements
|
|
13
|
+
*/
|
|
14
|
+
export declare const VirColorPair: import("element-vir").DeclarativeElementDefinition<"theme-vir-color-example", {
|
|
15
|
+
color: Readonly<ColorPair>;
|
|
16
|
+
showVarValues: boolean;
|
|
17
|
+
showVarNames: boolean;
|
|
18
|
+
showContrast: boolean;
|
|
19
|
+
fontWeight: FontWeight;
|
|
20
|
+
}, {
|
|
21
|
+
previewElement: undefined | HTMLElement;
|
|
22
|
+
forceShowEverything: boolean;
|
|
23
|
+
}, {}, "theme-vir-color-example-no-contrast-tips", "theme-vir-color-example-", readonly [], readonly []>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/* node:coverage disable */
|
|
2
|
+
import { assertWrap, check } from '@augment-vir/assert';
|
|
3
|
+
import { css, defineElement, html, listen, nothing, onDomCreated, unsafeCSS } from 'element-vir';
|
|
4
|
+
import { noNativeFormStyles, noNativeSpacing } from 'vira';
|
|
5
|
+
import { calculateContrast } from '../contrast/contrast.js';
|
|
6
|
+
import { VirContrastIndicator } from './vir-contrast-indicator.element.js';
|
|
7
|
+
/**
|
|
8
|
+
* Showcase a foreground/backend color pair.
|
|
9
|
+
*
|
|
10
|
+
* @category Elements
|
|
11
|
+
*/
|
|
12
|
+
export const VirColorPair = defineElement()({
|
|
13
|
+
tagName: 'theme-vir-color-example',
|
|
14
|
+
state() {
|
|
15
|
+
return {
|
|
16
|
+
previewElement: undefined,
|
|
17
|
+
forceShowEverything: false,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
hostClasses: {
|
|
21
|
+
'theme-vir-color-example-no-contrast-tips': ({ inputs, state }) => !inputs.showContrast && !state.forceShowEverything,
|
|
22
|
+
},
|
|
23
|
+
styles: ({ hostClasses }) => css `
|
|
24
|
+
:host {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
align-items: center;
|
|
28
|
+
max-width: 100%;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.color-preview {
|
|
32
|
+
${noNativeFormStyles};
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
font-size: 32px;
|
|
35
|
+
padding-left: 12px;
|
|
36
|
+
padding-right: 0;
|
|
37
|
+
border: 1px solid #ccc;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 8px;
|
|
41
|
+
align-items: baseline;
|
|
42
|
+
|
|
43
|
+
& b {
|
|
44
|
+
margin: 12px 0;
|
|
45
|
+
font-weight: bold;
|
|
46
|
+
text-decoration: underline;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
& .square {
|
|
50
|
+
margin: 12px 0;
|
|
51
|
+
width: 24px;
|
|
52
|
+
height: 24px;
|
|
53
|
+
background-color: currentColor;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
${hostClasses['theme-vir-color-example-no-contrast-tips'].selector} {
|
|
57
|
+
& .needed-size-wrapper {
|
|
58
|
+
display: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
& .color-preview {
|
|
62
|
+
padding: 4px 24px;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.needed-size-wrapper {
|
|
67
|
+
align-self: stretch;
|
|
68
|
+
width: 56px;
|
|
69
|
+
position: relative;
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
border-left: 1px solid #ccc;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.needed-size {
|
|
75
|
+
top: 0;
|
|
76
|
+
height: 100%;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
left: 6px;
|
|
80
|
+
position: absolute;
|
|
81
|
+
|
|
82
|
+
& span {
|
|
83
|
+
margin: 0 auto;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.css-var-names {
|
|
88
|
+
font-family: monospace;
|
|
89
|
+
display: flex;
|
|
90
|
+
max-width: 100%;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
opacity: 0.6;
|
|
93
|
+
margin-top: 4px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
p {
|
|
97
|
+
${noNativeSpacing};
|
|
98
|
+
display: flex;
|
|
99
|
+
gap: 0;
|
|
100
|
+
flex-wrap: wrap;
|
|
101
|
+
|
|
102
|
+
& span:last-child {
|
|
103
|
+
margin-left: 1ex;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
${VirContrastIndicator} {
|
|
108
|
+
margin-top: 1px;
|
|
109
|
+
}
|
|
110
|
+
`,
|
|
111
|
+
render({ state, updateState, inputs }) {
|
|
112
|
+
const colorRows = [
|
|
113
|
+
'foreground',
|
|
114
|
+
'background',
|
|
115
|
+
].map((layerKey) => {
|
|
116
|
+
const keyString = [
|
|
117
|
+
inputs.color[layerKey].name,
|
|
118
|
+
inputs.showVarValues || state.forceShowEverything ? ':' : '',
|
|
119
|
+
]
|
|
120
|
+
.filter(check.isTruthy)
|
|
121
|
+
.join('');
|
|
122
|
+
const valueTemplate = inputs.showVarValues || state.forceShowEverything
|
|
123
|
+
? html `
|
|
124
|
+
<span>${inputs.color[layerKey].default}</span>
|
|
125
|
+
`
|
|
126
|
+
: nothing;
|
|
127
|
+
return html `
|
|
128
|
+
<p>
|
|
129
|
+
<span>${keyString}</span>
|
|
130
|
+
${valueTemplate}
|
|
131
|
+
</p>
|
|
132
|
+
`;
|
|
133
|
+
});
|
|
134
|
+
const cssVarNamesTemplate = inputs.showVarNames || state.forceShowEverything
|
|
135
|
+
? html `
|
|
136
|
+
<div class="css-var-names">${colorRows}</div>
|
|
137
|
+
`
|
|
138
|
+
: nothing;
|
|
139
|
+
const contrast = state.previewElement
|
|
140
|
+
? calculateContrast({
|
|
141
|
+
foreground: globalThis
|
|
142
|
+
.getComputedStyle(state.previewElement)
|
|
143
|
+
.getPropertyValue('color'),
|
|
144
|
+
background: globalThis
|
|
145
|
+
.getComputedStyle(state.previewElement)
|
|
146
|
+
.getPropertyValue('background-color'),
|
|
147
|
+
})
|
|
148
|
+
: undefined;
|
|
149
|
+
const contrastTemplate = contrast && (inputs.showContrast || state.forceShowEverything)
|
|
150
|
+
? html `
|
|
151
|
+
<${VirContrastIndicator.assign({
|
|
152
|
+
contrast,
|
|
153
|
+
fontWeight: inputs.fontWeight,
|
|
154
|
+
})}></${VirContrastIndicator}>
|
|
155
|
+
`
|
|
156
|
+
: nothing;
|
|
157
|
+
return html `
|
|
158
|
+
<button
|
|
159
|
+
${listen('click', () => {
|
|
160
|
+
updateState({
|
|
161
|
+
forceShowEverything: !state.forceShowEverything,
|
|
162
|
+
});
|
|
163
|
+
})}
|
|
164
|
+
${onDomCreated((element) => {
|
|
165
|
+
updateState({
|
|
166
|
+
previewElement: assertWrap.instanceOf(element, HTMLElement),
|
|
167
|
+
});
|
|
168
|
+
})}
|
|
169
|
+
class="color-preview"
|
|
170
|
+
style=${css `
|
|
171
|
+
color: ${unsafeCSS(inputs.color.foreground.default)};
|
|
172
|
+
background: ${unsafeCSS(inputs.color.background.default)};
|
|
173
|
+
`}
|
|
174
|
+
>
|
|
175
|
+
<div class="square"></div>
|
|
176
|
+
<b>Aa</b>
|
|
177
|
+
<div class="needed-size-wrapper">
|
|
178
|
+
<span class="needed-size">
|
|
179
|
+
<span
|
|
180
|
+
style=${css `
|
|
181
|
+
visibility: ${unsafeCSS((contrast?.fontSizes[400] || Infinity) > 150
|
|
182
|
+
? 'hidden'
|
|
183
|
+
: 'visible')};
|
|
184
|
+
font-weight: ${inputs.fontWeight};
|
|
185
|
+
font-size: ${contrast ? contrast.fontSizes[400] : 14}px;
|
|
186
|
+
`}
|
|
187
|
+
>
|
|
188
|
+
Min
|
|
189
|
+
</span>
|
|
190
|
+
</span>
|
|
191
|
+
</div>
|
|
192
|
+
</button>
|
|
193
|
+
${contrastTemplate} ${cssVarNamesTemplate}
|
|
194
|
+
`;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ColorCoordinateName, type ColorFormatName } from '../color-class/color-formats.js';
|
|
2
|
+
import { Color } from '../color-class/color.js';
|
|
3
|
+
/**
|
|
4
|
+
* A slider for a specific color coordinate in a specific color space in a specific color.
|
|
5
|
+
*
|
|
6
|
+
* @category Elements
|
|
7
|
+
*/
|
|
8
|
+
export declare const VirColorSlider: import("element-vir").DeclarativeElementDefinition<"vir-color-slider", {
|
|
9
|
+
color: Readonly<Color>;
|
|
10
|
+
colorFormatName: ColorFormatName;
|
|
11
|
+
colorCoordinateName: ColorCoordinateName;
|
|
12
|
+
}, {}, {
|
|
13
|
+
valueChange: import("element-vir").DefineEvent<number>;
|
|
14
|
+
}, "vir-color-slider-", "vir-color-slider-gradient", readonly [], readonly []>;
|