theme-vir 25.6.0 → 25.7.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/README.md +2 -0
- package/dist/color/color-theme-book-pages.d.ts +23 -0
- package/dist/color/color-theme-book-pages.js +107 -0
- package/dist/color/color-theme-override.d.ts +13 -8
- package/dist/color/color-theme-override.js +21 -9
- package/dist/color/color-theme.d.ts +31 -4
- package/dist/color/color-theme.js +91 -31
- package/dist/color/contrast.d.ts +180 -0
- package/dist/color/contrast.js +163 -0
- package/dist/color/elements/theme-vir-color-example.element.d.ts +16 -0
- package/dist/color/elements/theme-vir-color-example.element.js +193 -0
- package/dist/color/elements/theme-vir-contrast-indicator.element.d.ts +9 -0
- package/dist/color/elements/theme-vir-contrast-indicator.element.js +93 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/package.json +5 -3
- package/dist/theme-book/vir-theme-book-app.element.d.ts +0 -3
- package/dist/theme-book/vir-theme-book-app.element.js +0 -34
package/README.md
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type PartialWithUndefined } from '@augment-vir/common';
|
|
2
|
+
import { BookPageControlType, type BookPage } from 'element-book';
|
|
3
|
+
import { type ColorThemeOverride } from './color-theme-override.js';
|
|
4
|
+
import { type ColorTheme } from './color-theme.js';
|
|
5
|
+
/**
|
|
6
|
+
* Create multiple element-book pages to showcase a theme its overrides (if any).
|
|
7
|
+
*
|
|
8
|
+
* @category Color Theme
|
|
9
|
+
*/
|
|
10
|
+
export declare function createColorThemeBookPages({ parent, title, theme, hideInverseColors, overrides, }: {
|
|
11
|
+
title: string;
|
|
12
|
+
theme: Readonly<ColorTheme>;
|
|
13
|
+
} & PartialWithUndefined<{
|
|
14
|
+
parent: Readonly<BookPage>;
|
|
15
|
+
hideInverseColors: boolean;
|
|
16
|
+
overrides: ReadonlyArray<Readonly<ColorThemeOverride>>;
|
|
17
|
+
}>): (BookPage<{}, Readonly<BookPage> | undefined, {
|
|
18
|
+
readonly 'Show Var Names': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
|
|
19
|
+
readonly 'Show Contrast Tips': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
|
|
20
|
+
}> | BookPage<{}, BookPage<{}, Readonly<BookPage> | undefined, {
|
|
21
|
+
readonly 'Show Var Names': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
|
|
22
|
+
readonly 'Show Contrast Tips': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
|
|
23
|
+
}>, {}>)[];
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { BookPageControlType, defineBookPage, definePageControl, } from 'element-book';
|
|
2
|
+
import { css, html, listen, nothing } from 'element-vir';
|
|
3
|
+
import { ThemeVirColorExample } from './elements/theme-vir-color-example.element.js';
|
|
4
|
+
/**
|
|
5
|
+
* Create multiple element-book pages to showcase a theme its overrides (if any).
|
|
6
|
+
*
|
|
7
|
+
* @category Color Theme
|
|
8
|
+
*/
|
|
9
|
+
export function createColorThemeBookPages({ parent, title, theme, hideInverseColors, overrides, }) {
|
|
10
|
+
const themeParentPage = defineBookPage({
|
|
11
|
+
parent,
|
|
12
|
+
title,
|
|
13
|
+
controls: {
|
|
14
|
+
'Show Var Names': definePageControl({
|
|
15
|
+
controlType: BookPageControlType.Checkbox,
|
|
16
|
+
initValue: false,
|
|
17
|
+
}),
|
|
18
|
+
'Show Contrast Tips': definePageControl({
|
|
19
|
+
controlType: BookPageControlType.Checkbox,
|
|
20
|
+
initValue: true,
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
function createThemePage(defineExample, theme) {
|
|
25
|
+
Object.values(theme.colors).forEach((themeColor) => {
|
|
26
|
+
defineExample({
|
|
27
|
+
title: themeColor.name,
|
|
28
|
+
styles: css `
|
|
29
|
+
:host {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
gap: 4px;
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
state() {
|
|
36
|
+
return {
|
|
37
|
+
forceShowEverything: false,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
render({ controls, state, updateState }) {
|
|
41
|
+
const normalTemplate = html `
|
|
42
|
+
<${ThemeVirColorExample.assign({
|
|
43
|
+
color: themeColor,
|
|
44
|
+
showVarValues: true,
|
|
45
|
+
showVarNames: controls['Show Var Names'] || state.forceShowEverything,
|
|
46
|
+
showContrast: controls['Show Contrast Tips'] || state.forceShowEverything,
|
|
47
|
+
})}
|
|
48
|
+
${listen(ThemeVirColorExample.events.toggleShowVars, () => {
|
|
49
|
+
updateState({
|
|
50
|
+
forceShowEverything: !state.forceShowEverything,
|
|
51
|
+
});
|
|
52
|
+
})}
|
|
53
|
+
></${ThemeVirColorExample}>
|
|
54
|
+
`;
|
|
55
|
+
const inverseColor = hideInverseColors
|
|
56
|
+
? undefined
|
|
57
|
+
: theme.inverse[themeColor.name];
|
|
58
|
+
const inverseTemplate = inverseColor
|
|
59
|
+
? html `
|
|
60
|
+
<${ThemeVirColorExample.assign({
|
|
61
|
+
color: inverseColor,
|
|
62
|
+
showVarValues: false,
|
|
63
|
+
showVarNames: controls['Show Var Names'] || state.forceShowEverything,
|
|
64
|
+
showContrast: controls['Show Contrast Tips'] || state.forceShowEverything,
|
|
65
|
+
})}
|
|
66
|
+
${listen(ThemeVirColorExample.events.toggleShowVars, () => {
|
|
67
|
+
updateState({
|
|
68
|
+
forceShowEverything: !state.forceShowEverything,
|
|
69
|
+
});
|
|
70
|
+
})}
|
|
71
|
+
></${ThemeVirColorExample}>
|
|
72
|
+
`
|
|
73
|
+
: nothing;
|
|
74
|
+
return html `
|
|
75
|
+
${normalTemplate}${inverseTemplate}
|
|
76
|
+
`;
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const descriptionParagraphs = [
|
|
82
|
+
'Click a color preview to show CSS var names and values.',
|
|
83
|
+
];
|
|
84
|
+
const defaultThemePage = defineBookPage({
|
|
85
|
+
parent: themeParentPage,
|
|
86
|
+
title: 'Default Theme',
|
|
87
|
+
descriptionParagraphs,
|
|
88
|
+
defineExamples({ defineExample }) {
|
|
89
|
+
createThemePage(defineExample, theme);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const overridePages = (overrides || []).map((override) => {
|
|
93
|
+
return defineBookPage({
|
|
94
|
+
parent: themeParentPage,
|
|
95
|
+
title: override.name,
|
|
96
|
+
descriptionParagraphs,
|
|
97
|
+
defineExamples({ defineExample }) {
|
|
98
|
+
createThemePage(defineExample, override.asTheme);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
return [
|
|
103
|
+
themeParentPage,
|
|
104
|
+
defaultThemePage,
|
|
105
|
+
...overridePages,
|
|
106
|
+
];
|
|
107
|
+
}
|
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
import { type CssVarName } from 'lit-css-vars';
|
|
2
2
|
import { type RequireAtLeastOne } from 'type-fest';
|
|
3
|
-
import { type ColorInit, type ColorTheme } from './color-theme.js';
|
|
3
|
+
import { type ColorInit, type ColorTheme, type ColorThemeInit, type NoRefColorInit } from './color-theme.js';
|
|
4
4
|
/**
|
|
5
5
|
* Input for {@link defineColorThemeOverride} color overrides.
|
|
6
6
|
*
|
|
7
7
|
* @category Internal
|
|
8
8
|
*/
|
|
9
9
|
export type ColorThemeOverrideInit<Theme extends ColorTheme = ColorTheme> = Omit<Partial<{
|
|
10
|
-
[ColorName in keyof Theme]: ColorInit;
|
|
10
|
+
[ColorName in keyof Theme['colors']]: ColorInit;
|
|
11
11
|
}>, 'default'>;
|
|
12
12
|
/**
|
|
13
13
|
* Output of {@link defineColorThemeOverride}.
|
|
14
14
|
*
|
|
15
15
|
* @category Internal
|
|
16
16
|
*/
|
|
17
|
-
export type
|
|
17
|
+
export type ColorThemeOverride<Init extends ColorThemeInit = ColorThemeInit> = {
|
|
18
|
+
name: string;
|
|
19
|
+
overrides: Record<CssVarName, string>;
|
|
20
|
+
originalTheme: ColorTheme<Init>;
|
|
21
|
+
asTheme: ColorTheme<Init>;
|
|
22
|
+
};
|
|
18
23
|
/**
|
|
19
24
|
* Define a color theme override. Use this to define multiple theme variations, like light mode vs
|
|
20
25
|
* dark mode.
|
|
21
26
|
*
|
|
22
27
|
* @category Color Theme
|
|
23
28
|
*/
|
|
24
|
-
export declare function defineColorThemeOverride<const
|
|
29
|
+
export declare function defineColorThemeOverride<const Init extends ColorThemeInit>(originalTheme: ColorTheme<Init>, overrideName: string, { defaultOverride, colorOverrides, }: Readonly<RequireAtLeastOne<{
|
|
25
30
|
/** Override the default foreground and/or background colors. */
|
|
26
|
-
defaultOverride:
|
|
27
|
-
colorOverrides: ColorThemeOverrideInit<
|
|
28
|
-
}
|
|
31
|
+
defaultOverride: Readonly<NoRefColorInit>;
|
|
32
|
+
colorOverrides: Readonly<ColorThemeOverrideInit<ColorTheme<Init>>>;
|
|
33
|
+
}>>): ColorThemeOverride<Init>;
|
|
29
34
|
/**
|
|
30
35
|
* Set all color theme CSS vars on the given element. If no override is given, the theme color
|
|
31
36
|
* default values are assigned.
|
|
@@ -34,4 +39,4 @@ export declare function defineColorThemeOverride<const Theme extends ColorTheme>
|
|
|
34
39
|
*/
|
|
35
40
|
export declare function applyColorTheme<const Theme extends ColorTheme>(
|
|
36
41
|
/** This should usually be the top-level `html` element. */
|
|
37
|
-
element: HTMLElement, fullTheme: Theme, themeOverride?:
|
|
42
|
+
element: HTMLElement, fullTheme: Theme, themeOverride?: ColorThemeOverride | undefined): void;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { getObjectTypedEntries, getObjectTypedKeys, getObjectTypedValues } from '@augment-vir/common';
|
|
2
2
|
import { setCssVarValue } from 'lit-css-vars';
|
|
3
|
-
import { createColorCssVarDefault, } from './color-theme.js';
|
|
3
|
+
import { createColorCssVarDefault, defineColorTheme, themeDefaultKey, } from './color-theme.js';
|
|
4
4
|
function applyCssVarOverride({ originalTheme, layerKey, themeColor, override, overrideValues, }) {
|
|
5
5
|
const layerOverride = override?.[layerKey];
|
|
6
6
|
if (!layerOverride) {
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
|
-
overrideValues[String(themeColor[layerKey].name)] = String(createColorCssVarDefault(layerKey, layerOverride, originalTheme));
|
|
9
|
+
overrideValues[String(themeColor[layerKey].name)] = String(createColorCssVarDefault(layerKey, layerOverride, originalTheme.init.default, originalTheme.init.colors));
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Define a color theme override. Use this to define multiple theme variations, like light mode vs
|
|
@@ -14,7 +14,7 @@ function applyCssVarOverride({ originalTheme, layerKey, themeColor, override, ov
|
|
|
14
14
|
*
|
|
15
15
|
* @category Color Theme
|
|
16
16
|
*/
|
|
17
|
-
export function defineColorThemeOverride(originalTheme, { defaultOverride, colorOverrides, }) {
|
|
17
|
+
export function defineColorThemeOverride(originalTheme, overrideName, { defaultOverride, colorOverrides, }) {
|
|
18
18
|
const defaultValues = {};
|
|
19
19
|
if (defaultOverride) {
|
|
20
20
|
getObjectTypedKeys(defaultOverride).forEach((layerKey) => {
|
|
@@ -22,7 +22,7 @@ export function defineColorThemeOverride(originalTheme, { defaultOverride, color
|
|
|
22
22
|
originalTheme,
|
|
23
23
|
layerKey,
|
|
24
24
|
override: defaultOverride,
|
|
25
|
-
themeColor: originalTheme.
|
|
25
|
+
themeColor: originalTheme.colors[themeDefaultKey],
|
|
26
26
|
overrideValues: defaultValues,
|
|
27
27
|
});
|
|
28
28
|
});
|
|
@@ -30,7 +30,7 @@ export function defineColorThemeOverride(originalTheme, { defaultOverride, color
|
|
|
30
30
|
const colorValues = {};
|
|
31
31
|
if (colorOverrides) {
|
|
32
32
|
getObjectTypedEntries(colorOverrides).forEach(([colorName, override,]) => {
|
|
33
|
-
const themeColor = originalTheme[colorName];
|
|
33
|
+
const themeColor = originalTheme.colors[colorName];
|
|
34
34
|
if (!themeColor) {
|
|
35
35
|
throw new Error(`Override color name '${String(colorName)}' does not exist in the theme being overridden.`);
|
|
36
36
|
}
|
|
@@ -50,9 +50,21 @@ export function defineColorThemeOverride(originalTheme, { defaultOverride, color
|
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
const asTheme = defineColorTheme({
|
|
54
|
+
...originalTheme.init.default,
|
|
55
|
+
...defaultOverride,
|
|
56
|
+
}, {
|
|
57
|
+
...originalTheme.init.colors,
|
|
58
|
+
...colorOverrides,
|
|
59
|
+
});
|
|
53
60
|
return {
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
name: overrideName,
|
|
62
|
+
overrides: {
|
|
63
|
+
...defaultValues,
|
|
64
|
+
...colorValues,
|
|
65
|
+
},
|
|
66
|
+
originalTheme,
|
|
67
|
+
asTheme,
|
|
56
68
|
};
|
|
57
69
|
}
|
|
58
70
|
/**
|
|
@@ -64,7 +76,7 @@ export function defineColorThemeOverride(originalTheme, { defaultOverride, color
|
|
|
64
76
|
export function applyColorTheme(
|
|
65
77
|
/** This should usually be the top-level `html` element. */
|
|
66
78
|
element, fullTheme, themeOverride) {
|
|
67
|
-
getObjectTypedValues(fullTheme).forEach((themeColor) => {
|
|
79
|
+
getObjectTypedValues(fullTheme.colors).forEach((themeColor) => {
|
|
68
80
|
applyIndividualThemeColorValue({
|
|
69
81
|
element,
|
|
70
82
|
layerKey: 'background',
|
|
@@ -80,7 +92,7 @@ element, fullTheme, themeOverride) {
|
|
|
80
92
|
});
|
|
81
93
|
}
|
|
82
94
|
function applyIndividualThemeColorValue({ element, layerKey, themeOverride, themeColor, }) {
|
|
83
|
-
const override = themeOverride?.[String(themeColor[layerKey].name)];
|
|
95
|
+
const override = themeOverride?.overrides[String(themeColor[layerKey].name)];
|
|
84
96
|
const value = override || themeColor[layerKey].default;
|
|
85
97
|
setCssVarValue({
|
|
86
98
|
forCssVar: themeColor[layerKey],
|
|
@@ -26,6 +26,15 @@ export type ColorInit = RequireAtLeastOne<{
|
|
|
26
26
|
foreground: ColorInitValue;
|
|
27
27
|
background: ColorInitValue;
|
|
28
28
|
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Same as {@link ColorInit} but without references.
|
|
31
|
+
*
|
|
32
|
+
* @category Internal
|
|
33
|
+
*/
|
|
34
|
+
export type NoRefColorInit = RequireAtLeastOne<{
|
|
35
|
+
foreground: Exclude<ColorInitValue, ColorInitReference>;
|
|
36
|
+
background: Exclude<ColorInitValue, ColorInitReference>;
|
|
37
|
+
}>;
|
|
29
38
|
/**
|
|
30
39
|
* A defined individual color from a color theme.
|
|
31
40
|
*
|
|
@@ -34,6 +43,10 @@ export type ColorInit = RequireAtLeastOne<{
|
|
|
34
43
|
export type ColorThemeColor<Init extends ColorInit = ColorInit, Name extends CssVarName = CssVarName> = {
|
|
35
44
|
foreground: SingleCssVarDefinition;
|
|
36
45
|
background: SingleCssVarDefinition;
|
|
46
|
+
/**
|
|
47
|
+
* The name of this theme color within the theme itself. (This is not any of the CSS variable
|
|
48
|
+
* names.)
|
|
49
|
+
*/
|
|
37
50
|
name: Name;
|
|
38
51
|
init: Init;
|
|
39
52
|
};
|
|
@@ -49,26 +62,40 @@ export type ColorThemeInit = Record<CssVarName, ColorInit>;
|
|
|
49
62
|
* @category Internal
|
|
50
63
|
*/
|
|
51
64
|
export type ColorTheme<Init extends ColorThemeInit = ColorThemeInit> = {
|
|
65
|
+
colors: AllColorThemeColors<Init>;
|
|
66
|
+
inverse: AllColorThemeColors<Init>;
|
|
67
|
+
/** The original init object for this theme. */
|
|
68
|
+
init: {
|
|
69
|
+
colors: Init;
|
|
70
|
+
default: RequiredAndNotNull<NoRefColorInit>;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* All colors within a {@link ColorTheme}.
|
|
75
|
+
*
|
|
76
|
+
* @category Internal
|
|
77
|
+
*/
|
|
78
|
+
export type AllColorThemeColors<Init extends ColorThemeInit = ColorThemeInit> = {
|
|
52
79
|
[ColorName in keyof Init as ColorName extends CssVarName ? ColorName : never]: ColorName extends CssVarName ? Init[ColorName] extends ColorInit ? ColorThemeColor<Init[ColorName], ColorName> : never : never;
|
|
53
80
|
} & {
|
|
54
|
-
[themeDefaultKey]:
|
|
81
|
+
[themeDefaultKey]: ColorThemeColor<RequiredAndNotNull<NoRefColorInit>, typeof themeDefaultKey>;
|
|
55
82
|
};
|
|
56
83
|
/**
|
|
57
84
|
* Handles a color init value.
|
|
58
85
|
*
|
|
59
86
|
* @category Internal
|
|
60
87
|
*/
|
|
61
|
-
export declare function createColorCssVarDefault(fromName: string, init: ColorInitValue,
|
|
88
|
+
export declare function createColorCssVarDefault(fromName: string, init: ColorInitValue, defaultInit: RequiredAndNotNull<NoRefColorInit>, colorsInit: ColorThemeInit): Exclude<ColorInitValue, ColorInitReference>;
|
|
62
89
|
/**
|
|
63
90
|
* Default foreground/background color theme used in {@link ColorTheme}. Do not define a theme color
|
|
64
91
|
* with this name!
|
|
65
92
|
*
|
|
66
93
|
* @category Internal
|
|
67
94
|
*/
|
|
68
|
-
export declare const themeDefaultKey = "default";
|
|
95
|
+
export declare const themeDefaultKey = "theme-default";
|
|
69
96
|
/**
|
|
70
97
|
* Define a color theme.
|
|
71
98
|
*
|
|
72
99
|
* @category Color Theme
|
|
73
100
|
*/
|
|
74
|
-
export declare function defineColorTheme<const Init extends ColorThemeInit>(defaultInit: RequiredAndNotNull<
|
|
101
|
+
export declare function defineColorTheme<const Init extends ColorThemeInit>(defaultInit: RequiredAndNotNull<NoRefColorInit>, allColorsInit: Init): ColorTheme<Init>;
|
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import { assert, check } from '@augment-vir/assert';
|
|
2
|
-
import { getObjectTypedEntries
|
|
2
|
+
import { getObjectTypedEntries } from '@augment-vir/common';
|
|
3
3
|
import { defineCssVars, } from 'lit-css-vars';
|
|
4
4
|
/**
|
|
5
5
|
* Handles a color init value.
|
|
6
6
|
*
|
|
7
7
|
* @category Internal
|
|
8
8
|
*/
|
|
9
|
-
export function createColorCssVarDefault(fromName, init,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export function createColorCssVarDefault(fromName, init, defaultInit, colorsInit) {
|
|
10
|
+
const referenceKey = check.hasKey(init, 'refBackground')
|
|
11
|
+
? 'refBackground'
|
|
12
|
+
: check.hasKey(init, 'refForeground')
|
|
13
|
+
? 'refForeground'
|
|
14
|
+
: undefined;
|
|
15
|
+
const reference = referenceKey && check.hasKey(init, referenceKey) ? init[referenceKey] : undefined;
|
|
16
|
+
if (reference) {
|
|
17
|
+
const layerKey = referenceKey === 'refBackground' ? 'background' : 'foreground';
|
|
18
|
+
const referenced = colorsInit[reference];
|
|
19
|
+
if (!referenced) {
|
|
20
|
+
throw new Error(`Color theme ${referenceKey} reference '${reference}' does not exist. (Referenced from '${fromName}'.)`);
|
|
13
21
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
return `var(--${init.refForeground}-fg)`;
|
|
22
|
+
const colorValue = referenced[layerKey] ||
|
|
23
|
+
(layerKey === 'foreground'
|
|
24
|
+
? createColorCssVarDefault('default-fg', defaultInit.foreground, defaultInit, colorsInit)
|
|
25
|
+
: createColorCssVarDefault('default-bg', defaultInit.background, defaultInit, colorsInit));
|
|
26
|
+
return `var(--${reference}-${layerKey === 'foreground' ? 'fg' : 'bg'}, ${createColorCssVarDefault(reference, colorValue, defaultInit, colorsInit)})`;
|
|
21
27
|
}
|
|
22
28
|
else {
|
|
23
29
|
return init;
|
|
@@ -29,7 +35,7 @@ export function createColorCssVarDefault(fromName, init, fullInit) {
|
|
|
29
35
|
*
|
|
30
36
|
* @category Internal
|
|
31
37
|
*/
|
|
32
|
-
export const themeDefaultKey = 'default';
|
|
38
|
+
export const themeDefaultKey = 'theme-default';
|
|
33
39
|
/**
|
|
34
40
|
* Define a color theme.
|
|
35
41
|
*
|
|
@@ -40,22 +46,29 @@ export function defineColorTheme(defaultInit, allColorsInit) {
|
|
|
40
46
|
throw new Error(`Cannot define theme color by name '${themeDefaultKey}', it is used internally.`);
|
|
41
47
|
}
|
|
42
48
|
const defaultColors = defineCssVars({
|
|
43
|
-
'default-
|
|
44
|
-
'default-
|
|
49
|
+
'default-fg': createColorCssVarDefault('default-fg', defaultInit.foreground, defaultInit, allColorsInit),
|
|
50
|
+
'default-bg': createColorCssVarDefault('default-bg', defaultInit.background, defaultInit, allColorsInit),
|
|
51
|
+
'default-inverse-fg': createColorCssVarDefault('default-inverse-fg', defaultInit.background, defaultInit, allColorsInit),
|
|
52
|
+
'default-inverse-bg': createColorCssVarDefault('default-inverse-bg', defaultInit.foreground, defaultInit, allColorsInit),
|
|
45
53
|
});
|
|
46
54
|
const cssVarsSetup = getObjectTypedEntries(allColorsInit).reduce((accum, [colorName, colorInit,]) => {
|
|
47
|
-
|
|
55
|
+
const names = createCssVarNames(colorName);
|
|
56
|
+
accum[names.foreground] = colorInit.foreground
|
|
48
57
|
? createColorCssVarDefault([
|
|
49
58
|
colorName,
|
|
50
59
|
'foreground',
|
|
51
|
-
].join(' '), colorInit.foreground, allColorsInit)
|
|
60
|
+
].join(' '), colorInit.foreground, defaultInit, allColorsInit)
|
|
52
61
|
: `var(${defaultColors['default-fg'].name}, ${defaultColors['default-fg'].default})`;
|
|
53
|
-
accum[
|
|
62
|
+
accum[names.background] = colorInit.background
|
|
54
63
|
? createColorCssVarDefault([
|
|
55
64
|
colorName,
|
|
56
65
|
'background',
|
|
57
|
-
].join(' '), colorInit.background, allColorsInit)
|
|
66
|
+
].join(' '), colorInit.background, defaultInit, allColorsInit)
|
|
58
67
|
: `var(${defaultColors['default-bg'].name}, ${defaultColors['default-bg'].default})`;
|
|
68
|
+
accum[names.foregroundInverse] =
|
|
69
|
+
`var(--${names.background}, ${accum[names.background]})`;
|
|
70
|
+
accum[names.backgroundInverse] =
|
|
71
|
+
`var(--${names.foreground}, ${accum[names.foreground]})`;
|
|
59
72
|
return accum;
|
|
60
73
|
}, {});
|
|
61
74
|
/**
|
|
@@ -64,30 +77,77 @@ export function defineColorTheme(defaultInit, allColorsInit) {
|
|
|
64
77
|
* `cssVars` object is not directly exposed.
|
|
65
78
|
*/
|
|
66
79
|
const cssVars = defineCssVars(cssVarsSetup);
|
|
67
|
-
const colors =
|
|
80
|
+
const colors = {};
|
|
81
|
+
const inverseColors = {};
|
|
82
|
+
getObjectTypedEntries(allColorsInit).forEach(([colorName, colorInit,]) => {
|
|
68
83
|
assert.isString(colorName);
|
|
69
|
-
const names =
|
|
70
|
-
foreground: (colorName + '-fg'),
|
|
71
|
-
background: (colorName + '-bg'),
|
|
72
|
-
};
|
|
73
|
-
const background = cssVars[names.background];
|
|
84
|
+
const names = createCssVarNames(colorName);
|
|
74
85
|
const foreground = cssVars[names.foreground];
|
|
75
|
-
|
|
86
|
+
const background = cssVars[names.background];
|
|
87
|
+
const foregroundInverse = cssVars[names.foregroundInverse];
|
|
88
|
+
const backgroundInverse = cssVars[names.backgroundInverse];
|
|
76
89
|
assert.isDefined(foreground);
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
assert.isDefined(background);
|
|
91
|
+
assert.isDefined(foregroundInverse);
|
|
92
|
+
assert.isDefined(backgroundInverse);
|
|
93
|
+
colors[colorName] = {
|
|
79
94
|
foreground,
|
|
95
|
+
background,
|
|
96
|
+
init: colorInit,
|
|
97
|
+
name: colorName,
|
|
98
|
+
};
|
|
99
|
+
inverseColors[colorName] = {
|
|
100
|
+
foreground: foregroundInverse,
|
|
101
|
+
background: backgroundInverse,
|
|
80
102
|
init: colorInit,
|
|
81
103
|
name: colorName,
|
|
82
104
|
};
|
|
83
105
|
});
|
|
84
|
-
const
|
|
106
|
+
const themeDefaultColors = {
|
|
85
107
|
foreground: defaultColors['default-fg'],
|
|
86
108
|
background: defaultColors['default-bg'],
|
|
87
109
|
init: defaultInit,
|
|
110
|
+
name: themeDefaultKey,
|
|
88
111
|
};
|
|
112
|
+
const themeDefaultInverseColors = {
|
|
113
|
+
...themeDefaultColors,
|
|
114
|
+
foreground: defaultColors['default-inverse-fg'],
|
|
115
|
+
background: defaultColors['default-inverse-bg'],
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
colors: {
|
|
119
|
+
[themeDefaultKey]: themeDefaultColors,
|
|
120
|
+
...colors,
|
|
121
|
+
},
|
|
122
|
+
inverse: {
|
|
123
|
+
[themeDefaultKey]: themeDefaultInverseColors,
|
|
124
|
+
...inverseColors,
|
|
125
|
+
},
|
|
126
|
+
init: {
|
|
127
|
+
colors: allColorsInit,
|
|
128
|
+
default: defaultInit,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function createCssVarNames(colorName) {
|
|
89
133
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
134
|
+
foreground: [
|
|
135
|
+
colorName,
|
|
136
|
+
'fg',
|
|
137
|
+
].join('-'),
|
|
138
|
+
background: [
|
|
139
|
+
colorName,
|
|
140
|
+
'bg',
|
|
141
|
+
].join('-'),
|
|
142
|
+
foregroundInverse: [
|
|
143
|
+
colorName,
|
|
144
|
+
'inverse',
|
|
145
|
+
'fg',
|
|
146
|
+
].join('-'),
|
|
147
|
+
backgroundInverse: [
|
|
148
|
+
colorName,
|
|
149
|
+
'inverse',
|
|
150
|
+
'bg',
|
|
151
|
+
].join('-'),
|
|
92
152
|
};
|
|
93
153
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { type ArrayElement } from '@augment-vir/common';
|
|
2
|
+
/**
|
|
3
|
+
* All font weights that font sizes are calculated for. Used in {@link FontSizes} and
|
|
4
|
+
* {@link calculateFontSizes}.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export type FontSizeWeights = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
|
|
9
|
+
/**
|
|
10
|
+
* A mapping of font weights to font sizes. Used in {@link calculateFontSizes}.
|
|
11
|
+
*
|
|
12
|
+
* @category Internal
|
|
13
|
+
*/
|
|
14
|
+
export type FontSizes = Record<FontSizeWeights, number>;
|
|
15
|
+
/**
|
|
16
|
+
* Contrast calculations produced by {@link calculateContrast}.
|
|
17
|
+
*
|
|
18
|
+
* @category Internal
|
|
19
|
+
*/
|
|
20
|
+
export type CalculatedContrast = {
|
|
21
|
+
/** The raw APCA LC contrast value. */
|
|
22
|
+
contrast: number;
|
|
23
|
+
/** The minimum font size for each font weight for the current `contrast` value. */
|
|
24
|
+
fontSizes: FontSizes;
|
|
25
|
+
/** The contrast level for the current `contrast` value. */
|
|
26
|
+
contrastLevel: ContrastLevel;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A color pair of foreground and background colors. The value of each (a string) may be any valid
|
|
30
|
+
* CSS color string.
|
|
31
|
+
*
|
|
32
|
+
* @category Internal
|
|
33
|
+
*/
|
|
34
|
+
export type ColorPair = {
|
|
35
|
+
foreground: string;
|
|
36
|
+
background: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Calculate contrast for the given color combination.
|
|
40
|
+
*
|
|
41
|
+
* @category Internal
|
|
42
|
+
*/
|
|
43
|
+
export declare function calculateContrast({ background, foreground, }: Readonly<ColorPair>): CalculatedContrast;
|
|
44
|
+
/**
|
|
45
|
+
* Calculated needed font sizes for each font weight for the given color contrast.
|
|
46
|
+
*
|
|
47
|
+
* @category Internal
|
|
48
|
+
*/
|
|
49
|
+
export declare function calculateFontSizes(contrast: number): FontSizes;
|
|
50
|
+
/**
|
|
51
|
+
* Finds the color contrast level for the given contrast.
|
|
52
|
+
*
|
|
53
|
+
* @category Internal
|
|
54
|
+
*/
|
|
55
|
+
export declare function determineContrastLevel(contrast: number): ContrastLevel;
|
|
56
|
+
/**
|
|
57
|
+
* Names for each {@link ContrastLevel}.
|
|
58
|
+
*
|
|
59
|
+
* @category Internal
|
|
60
|
+
*/
|
|
61
|
+
export declare enum ContrastLevelName {
|
|
62
|
+
SmallBodyText = "small-body-text",
|
|
63
|
+
BodyText = "body-text",
|
|
64
|
+
NonBodyText = "non-body-text",
|
|
65
|
+
LargeText = "large-text",
|
|
66
|
+
SpotText = "spot-text",
|
|
67
|
+
Decoration = "decoration",
|
|
68
|
+
Invisible = "invisible"
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* User-facing labels for {@link ContrastLevelName}.
|
|
72
|
+
*
|
|
73
|
+
* @category Internal
|
|
74
|
+
*/
|
|
75
|
+
export declare const contrastLevelLabel: Record<ContrastLevelName, string>;
|
|
76
|
+
/**
|
|
77
|
+
* All {@link ContrastLevelName} values in order from highest contrast to lowest.
|
|
78
|
+
*
|
|
79
|
+
* @category Internal
|
|
80
|
+
*/
|
|
81
|
+
export declare const orderedContrastLevelNames: readonly [ContrastLevelName.SmallBodyText, ContrastLevelName.BodyText, ContrastLevelName.NonBodyText, ContrastLevelName.LargeText, ContrastLevelName.SpotText, ContrastLevelName.Decoration, ContrastLevelName.Invisible];
|
|
82
|
+
/**
|
|
83
|
+
* Color contrast level details.
|
|
84
|
+
*
|
|
85
|
+
* @category Internal
|
|
86
|
+
*/
|
|
87
|
+
export type ContrastLevel = {
|
|
88
|
+
/** The minimum contrast level threshold for this contrast level. */
|
|
89
|
+
min: number;
|
|
90
|
+
/** The name corresponding to the smallest text or non-text item that this can be used for. */
|
|
91
|
+
name: ContrastLevelName;
|
|
92
|
+
/** Short description for this contrast level. */
|
|
93
|
+
description: string;
|
|
94
|
+
/** Name from the APCA guidelines (confusing). */
|
|
95
|
+
apcaName: string;
|
|
96
|
+
/** Description from the APCA guidelines (verbose). */
|
|
97
|
+
apcaDescription: string;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* All color contrast levels corresponding to APCA bronze guidelines.
|
|
101
|
+
*
|
|
102
|
+
* @category Internal
|
|
103
|
+
*/
|
|
104
|
+
export declare const contrastLevels: readonly [{
|
|
105
|
+
readonly min: 90;
|
|
106
|
+
readonly name: ContrastLevelName.SmallBodyText;
|
|
107
|
+
readonly description: "Perfect for all sizes of text, even small body text.";
|
|
108
|
+
readonly apcaName: "small body text only";
|
|
109
|
+
readonly 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.";
|
|
110
|
+
}, {
|
|
111
|
+
readonly min: 75;
|
|
112
|
+
readonly name: ContrastLevelName.BodyText;
|
|
113
|
+
readonly description: "Good for regular body text and anything larger.";
|
|
114
|
+
readonly apcaName: "body text okay";
|
|
115
|
+
readonly 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.";
|
|
116
|
+
}, {
|
|
117
|
+
readonly min: 60;
|
|
118
|
+
readonly name: ContrastLevelName.NonBodyText;
|
|
119
|
+
readonly description: "Good for legible non-body text and anything larger.";
|
|
120
|
+
readonly apcaName: "fluent text only";
|
|
121
|
+
readonly 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.";
|
|
122
|
+
}, {
|
|
123
|
+
readonly min: 45;
|
|
124
|
+
readonly name: ContrastLevelName.LargeText;
|
|
125
|
+
readonly description: "Okay for large or headline text.";
|
|
126
|
+
readonly apcaName: "large & sub-fluent text";
|
|
127
|
+
readonly 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.";
|
|
128
|
+
}, {
|
|
129
|
+
readonly min: 30;
|
|
130
|
+
readonly name: ContrastLevelName.SpotText;
|
|
131
|
+
readonly description: "Okay for disabled or placeholder text, copyright lines, icons, or non-text elements.";
|
|
132
|
+
readonly apcaName: "spot & non text only";
|
|
133
|
+
readonly 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.";
|
|
134
|
+
}, {
|
|
135
|
+
readonly min: 15;
|
|
136
|
+
readonly name: ContrastLevelName.Decoration;
|
|
137
|
+
readonly description: "Only okay for decorations like graphics, borders, dividers, etc. Do not use for any text.";
|
|
138
|
+
readonly apcaName: "no text usage";
|
|
139
|
+
readonly 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.";
|
|
140
|
+
}, {
|
|
141
|
+
readonly min: 0;
|
|
142
|
+
readonly name: ContrastLevelName.Invisible;
|
|
143
|
+
readonly description: "Effectively invisible for users.";
|
|
144
|
+
readonly apcaName: "invisible";
|
|
145
|
+
readonly apcaDescription: "This should be treated as invisible.";
|
|
146
|
+
}];
|
|
147
|
+
/**
|
|
148
|
+
* Type for {@link contrastLevelMinMap}.
|
|
149
|
+
*
|
|
150
|
+
* @category Internal
|
|
151
|
+
*/
|
|
152
|
+
export type ContrastLevelMinMap = {
|
|
153
|
+
[Min in ArrayElement<typeof contrastLevels>['min']]: Extract<ArrayElement<typeof contrastLevels>, {
|
|
154
|
+
min: Min;
|
|
155
|
+
}>;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* A mapping of all color contrast levels mins to their contrast levels. Generated from
|
|
159
|
+
* {@link contrastLevels}.
|
|
160
|
+
*
|
|
161
|
+
* @category Internal
|
|
162
|
+
*/
|
|
163
|
+
export declare const contrastLevelMinMap: ContrastLevelMinMap;
|
|
164
|
+
/**
|
|
165
|
+
* Type for {@link contrastLevelMinMap}.
|
|
166
|
+
*
|
|
167
|
+
* @category Internal
|
|
168
|
+
*/
|
|
169
|
+
export type ContrastLevelNameMap = {
|
|
170
|
+
[Name in ArrayElement<typeof contrastLevels>['name']]: Extract<ArrayElement<typeof contrastLevels>, {
|
|
171
|
+
name: Name;
|
|
172
|
+
}>;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* A mapping of all color contrast levels mins to their contrast levels. Generated from
|
|
176
|
+
* {@link contrastLevels}.
|
|
177
|
+
*
|
|
178
|
+
* @category Internal
|
|
179
|
+
*/
|
|
180
|
+
export declare const contrastLevelNameMap: ContrastLevelNameMap;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { assertWrap } from '@augment-vir/assert';
|
|
2
|
+
import { arrayToObject, round } from '@augment-vir/common';
|
|
3
|
+
// @ts-expect-error: `fontLookupAPCA` is not in the types
|
|
4
|
+
import { calcAPCA, fontLookupAPCA } from 'apca-w3';
|
|
5
|
+
/**
|
|
6
|
+
* Calculate contrast for the given color combination.
|
|
7
|
+
*
|
|
8
|
+
* @category Internal
|
|
9
|
+
*/
|
|
10
|
+
export function calculateContrast({ background, foreground, }) {
|
|
11
|
+
const contrast = round(Number(calcAPCA(foreground, background)), { digits: 1 });
|
|
12
|
+
return {
|
|
13
|
+
contrast,
|
|
14
|
+
fontSizes: calculateFontSizes(contrast),
|
|
15
|
+
contrastLevel: determineContrastLevel(contrast),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Calculated needed font sizes for each font weight for the given color contrast.
|
|
20
|
+
*
|
|
21
|
+
* @category Internal
|
|
22
|
+
*/
|
|
23
|
+
export function calculateFontSizes(contrast) {
|
|
24
|
+
const fontLookup = fontLookupAPCA(contrast).slice(1);
|
|
25
|
+
const sizes = arrayToObject(fontLookup, (value, index) => {
|
|
26
|
+
return {
|
|
27
|
+
key: (index + 1) * 100,
|
|
28
|
+
value,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
return sizes;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Finds the color contrast level for the given contrast.
|
|
35
|
+
*
|
|
36
|
+
* @category Internal
|
|
37
|
+
*/
|
|
38
|
+
export function determineContrastLevel(contrast) {
|
|
39
|
+
return assertWrap.isDefined(contrastLevels.find((threshold) => threshold.min <= Math.abs(contrast)));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Names for each {@link ContrastLevel}.
|
|
43
|
+
*
|
|
44
|
+
* @category Internal
|
|
45
|
+
*/
|
|
46
|
+
export var ContrastLevelName;
|
|
47
|
+
(function (ContrastLevelName) {
|
|
48
|
+
ContrastLevelName["SmallBodyText"] = "small-body-text";
|
|
49
|
+
ContrastLevelName["BodyText"] = "body-text";
|
|
50
|
+
ContrastLevelName["NonBodyText"] = "non-body-text";
|
|
51
|
+
ContrastLevelName["LargeText"] = "large-text";
|
|
52
|
+
ContrastLevelName["SpotText"] = "spot-text";
|
|
53
|
+
ContrastLevelName["Decoration"] = "decoration";
|
|
54
|
+
ContrastLevelName["Invisible"] = "invisible";
|
|
55
|
+
})(ContrastLevelName || (ContrastLevelName = {}));
|
|
56
|
+
/**
|
|
57
|
+
* User-facing labels for {@link ContrastLevelName}.
|
|
58
|
+
*
|
|
59
|
+
* @category Internal
|
|
60
|
+
*/
|
|
61
|
+
export const contrastLevelLabel = {
|
|
62
|
+
[ContrastLevelName.SmallBodyText]: 'Small Text',
|
|
63
|
+
[ContrastLevelName.BodyText]: 'Body Text',
|
|
64
|
+
[ContrastLevelName.NonBodyText]: 'Non-body Text',
|
|
65
|
+
[ContrastLevelName.LargeText]: 'Headers',
|
|
66
|
+
[ContrastLevelName.SpotText]: 'Placeholders',
|
|
67
|
+
[ContrastLevelName.Decoration]: 'Decorations',
|
|
68
|
+
[ContrastLevelName.Invisible]: 'Invisible ',
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* All {@link ContrastLevelName} values in order from highest contrast to lowest.
|
|
72
|
+
*
|
|
73
|
+
* @category Internal
|
|
74
|
+
*/
|
|
75
|
+
export const orderedContrastLevelNames = [
|
|
76
|
+
ContrastLevelName.SmallBodyText,
|
|
77
|
+
ContrastLevelName.BodyText,
|
|
78
|
+
ContrastLevelName.NonBodyText,
|
|
79
|
+
ContrastLevelName.LargeText,
|
|
80
|
+
ContrastLevelName.SpotText,
|
|
81
|
+
ContrastLevelName.Decoration,
|
|
82
|
+
ContrastLevelName.Invisible,
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* All color contrast levels corresponding to APCA bronze guidelines.
|
|
86
|
+
*
|
|
87
|
+
* @category Internal
|
|
88
|
+
*/
|
|
89
|
+
export const contrastLevels = [
|
|
90
|
+
{
|
|
91
|
+
min: 90,
|
|
92
|
+
name: ContrastLevelName.SmallBodyText,
|
|
93
|
+
description: 'Perfect for all sizes of text, even small body text.',
|
|
94
|
+
apcaName: 'small body text only',
|
|
95
|
+
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.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
min: 75,
|
|
99
|
+
name: ContrastLevelName.BodyText,
|
|
100
|
+
description: 'Good for regular body text and anything larger.',
|
|
101
|
+
apcaName: 'body text okay',
|
|
102
|
+
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.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
min: 60,
|
|
106
|
+
name: ContrastLevelName.NonBodyText,
|
|
107
|
+
description: 'Good for legible non-body text and anything larger.',
|
|
108
|
+
apcaName: 'fluent text only',
|
|
109
|
+
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.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
min: 45,
|
|
113
|
+
name: ContrastLevelName.LargeText,
|
|
114
|
+
description: 'Okay for large or headline text.',
|
|
115
|
+
apcaName: 'large & sub-fluent text',
|
|
116
|
+
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.',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
min: 30,
|
|
120
|
+
name: ContrastLevelName.SpotText,
|
|
121
|
+
description: 'Okay for disabled or placeholder text, copyright lines, icons, or non-text elements.',
|
|
122
|
+
apcaName: 'spot & non text only',
|
|
123
|
+
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.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
min: 15,
|
|
127
|
+
name: ContrastLevelName.Decoration,
|
|
128
|
+
description: 'Only okay for decorations like graphics, borders, dividers, etc. Do not use for any text.',
|
|
129
|
+
apcaName: 'no text usage',
|
|
130
|
+
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.',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
min: 0,
|
|
134
|
+
name: ContrastLevelName.Invisible,
|
|
135
|
+
description: 'Effectively invisible for users.',
|
|
136
|
+
apcaName: 'invisible',
|
|
137
|
+
apcaDescription: 'This should be treated as invisible.',
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
/**
|
|
141
|
+
* A mapping of all color contrast levels mins to their contrast levels. Generated from
|
|
142
|
+
* {@link contrastLevels}.
|
|
143
|
+
*
|
|
144
|
+
* @category Internal
|
|
145
|
+
*/
|
|
146
|
+
export const contrastLevelMinMap = arrayToObject(contrastLevels, (contrastLevel) => {
|
|
147
|
+
return {
|
|
148
|
+
key: contrastLevel.min,
|
|
149
|
+
value: contrastLevel,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
/**
|
|
153
|
+
* A mapping of all color contrast levels mins to their contrast levels. Generated from
|
|
154
|
+
* {@link contrastLevels}.
|
|
155
|
+
*
|
|
156
|
+
* @category Internal
|
|
157
|
+
*/
|
|
158
|
+
export const contrastLevelNameMap = arrayToObject(contrastLevels, (contrastLevel) => {
|
|
159
|
+
return {
|
|
160
|
+
key: contrastLevel.name,
|
|
161
|
+
value: contrastLevel,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ColorThemeColor } from '../color-theme.js';
|
|
2
|
+
/**
|
|
3
|
+
* Showcase a theme-vir color theme color.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const ThemeVirColorExample: import("element-vir").DeclarativeElementDefinition<"theme-vir-color-example", {
|
|
8
|
+
color: Readonly<Pick<ColorThemeColor, "foreground" | "background">>;
|
|
9
|
+
showVarValues: boolean;
|
|
10
|
+
showVarNames: boolean;
|
|
11
|
+
showContrast: boolean;
|
|
12
|
+
}, {
|
|
13
|
+
previewElement: undefined | HTMLElement;
|
|
14
|
+
}, {
|
|
15
|
+
toggleShowVars: import("element-vir").DefineEvent<void>;
|
|
16
|
+
}, "theme-vir-color-example-no-contrast-tips", "theme-vir-color-example-", readonly []>;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { assertWrap, check } from '@augment-vir/assert';
|
|
2
|
+
import { css, defineElement, defineElementEvent, html, listen, nothing, onDomCreated, unsafeCSS, } from 'element-vir';
|
|
3
|
+
import { noNativeFormStyles, noNativeSpacing } from 'vira';
|
|
4
|
+
import { calculateContrast } from '../contrast.js';
|
|
5
|
+
import { ThemeVirContrastIndicator } from './theme-vir-contrast-indicator.element.js';
|
|
6
|
+
/**
|
|
7
|
+
* Showcase a theme-vir color theme color.
|
|
8
|
+
*
|
|
9
|
+
* @category Internal
|
|
10
|
+
*/
|
|
11
|
+
export const ThemeVirColorExample = defineElement()({
|
|
12
|
+
tagName: 'theme-vir-color-example',
|
|
13
|
+
state() {
|
|
14
|
+
return {
|
|
15
|
+
previewElement: undefined,
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
events: {
|
|
19
|
+
toggleShowVars: defineElementEvent(),
|
|
20
|
+
},
|
|
21
|
+
hostClasses: {
|
|
22
|
+
'theme-vir-color-example-no-contrast-tips': ({ inputs }) => !inputs.showContrast,
|
|
23
|
+
},
|
|
24
|
+
styles: ({ hostClasses }) => css `
|
|
25
|
+
:host {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
align-items: center;
|
|
29
|
+
max-width: 100%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.color-preview {
|
|
33
|
+
${noNativeFormStyles};
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
font-size: 32px;
|
|
36
|
+
padding-left: 12px;
|
|
37
|
+
padding-right: 0;
|
|
38
|
+
border: 1px solid #ccc;
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
display: flex;
|
|
41
|
+
gap: 8px;
|
|
42
|
+
align-items: baseline;
|
|
43
|
+
|
|
44
|
+
& b {
|
|
45
|
+
margin: 12px 0;
|
|
46
|
+
font-weight: bold;
|
|
47
|
+
text-decoration: underline;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
& .square {
|
|
51
|
+
margin: 12px 0;
|
|
52
|
+
width: 24px;
|
|
53
|
+
height: 24px;
|
|
54
|
+
background-color: currentColor;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
${hostClasses['theme-vir-color-example-no-contrast-tips'].selector} {
|
|
58
|
+
& .needed-size-wrapper {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
& .color-preview {
|
|
63
|
+
padding: 4px 24px;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.needed-size-wrapper {
|
|
68
|
+
align-self: stretch;
|
|
69
|
+
width: 56px;
|
|
70
|
+
position: relative;
|
|
71
|
+
overflow: hidden;
|
|
72
|
+
border-left: 1px solid #ccc;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.needed-size {
|
|
76
|
+
top: 0;
|
|
77
|
+
height: 100%;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
left: 6px;
|
|
81
|
+
position: absolute;
|
|
82
|
+
|
|
83
|
+
& span {
|
|
84
|
+
margin: 0 auto;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.css-var-names {
|
|
89
|
+
font-family: monospace;
|
|
90
|
+
display: flex;
|
|
91
|
+
max-width: 100%;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
opacity: 0.6;
|
|
94
|
+
margin-top: 4px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
p {
|
|
98
|
+
${noNativeSpacing};
|
|
99
|
+
display: flex;
|
|
100
|
+
gap: 0;
|
|
101
|
+
flex-wrap: wrap;
|
|
102
|
+
|
|
103
|
+
& span:last-child {
|
|
104
|
+
margin-left: 1ex;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
${ThemeVirContrastIndicator} {
|
|
109
|
+
margin-top: 1px;
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
render({ state, updateState, inputs, dispatch, events }) {
|
|
113
|
+
const colorRows = [
|
|
114
|
+
'foreground',
|
|
115
|
+
'background',
|
|
116
|
+
].map((layerKey) => {
|
|
117
|
+
const keyString = [
|
|
118
|
+
inputs.color[layerKey].name,
|
|
119
|
+
inputs.showVarValues ? ':' : '',
|
|
120
|
+
]
|
|
121
|
+
.filter(check.isTruthy)
|
|
122
|
+
.join('');
|
|
123
|
+
const valueTemplate = inputs.showVarValues
|
|
124
|
+
? html `
|
|
125
|
+
<span>${inputs.color[layerKey].default}</span>
|
|
126
|
+
`
|
|
127
|
+
: nothing;
|
|
128
|
+
return html `
|
|
129
|
+
<p>
|
|
130
|
+
<span>${keyString}</span>
|
|
131
|
+
${valueTemplate}
|
|
132
|
+
</p>
|
|
133
|
+
`;
|
|
134
|
+
});
|
|
135
|
+
const cssVarNamesTemplate = inputs.showVarNames
|
|
136
|
+
? html `
|
|
137
|
+
<div class="css-var-names">${colorRows}</div>
|
|
138
|
+
`
|
|
139
|
+
: nothing;
|
|
140
|
+
const contrast = state.previewElement
|
|
141
|
+
? calculateContrast({
|
|
142
|
+
foreground: globalThis
|
|
143
|
+
.getComputedStyle(state.previewElement)
|
|
144
|
+
.getPropertyValue('color'),
|
|
145
|
+
background: globalThis
|
|
146
|
+
.getComputedStyle(state.previewElement)
|
|
147
|
+
.getPropertyValue('background-color'),
|
|
148
|
+
})
|
|
149
|
+
: undefined;
|
|
150
|
+
const contrastTemplate = contrast && inputs.showContrast
|
|
151
|
+
? html `
|
|
152
|
+
<${ThemeVirContrastIndicator.assign({
|
|
153
|
+
contrast,
|
|
154
|
+
})}></${ThemeVirContrastIndicator}>
|
|
155
|
+
`
|
|
156
|
+
: nothing;
|
|
157
|
+
return html `
|
|
158
|
+
<button
|
|
159
|
+
${listen('click', () => {
|
|
160
|
+
dispatch(new events.toggleShowVars());
|
|
161
|
+
})}
|
|
162
|
+
${onDomCreated((element) => {
|
|
163
|
+
updateState({
|
|
164
|
+
previewElement: assertWrap.instanceOf(element, HTMLElement),
|
|
165
|
+
});
|
|
166
|
+
})}
|
|
167
|
+
class="color-preview"
|
|
168
|
+
style=${css `
|
|
169
|
+
color: ${unsafeCSS(inputs.color.foreground.default)};
|
|
170
|
+
background: ${unsafeCSS(inputs.color.background.default)};
|
|
171
|
+
`}
|
|
172
|
+
>
|
|
173
|
+
<div class="square"></div>
|
|
174
|
+
<b>Aa</b>
|
|
175
|
+
<div class="needed-size-wrapper">
|
|
176
|
+
<span class="needed-size">
|
|
177
|
+
<span
|
|
178
|
+
style=${css `
|
|
179
|
+
visibility: ${unsafeCSS((contrast?.fontSizes[400] || Infinity) > 150
|
|
180
|
+
? 'hidden'
|
|
181
|
+
: 'visible')};
|
|
182
|
+
font-size: ${contrast ? contrast.fontSizes[400] : 14}px;
|
|
183
|
+
`}
|
|
184
|
+
>
|
|
185
|
+
Min
|
|
186
|
+
</span>
|
|
187
|
+
</span>
|
|
188
|
+
</div>
|
|
189
|
+
</button>
|
|
190
|
+
${contrastTemplate} ${cssVarNamesTemplate}
|
|
191
|
+
`;
|
|
192
|
+
},
|
|
193
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CalculatedContrast } from '../contrast.js';
|
|
2
|
+
/**
|
|
3
|
+
* Show contrast details for a theme-vir color.
|
|
4
|
+
*
|
|
5
|
+
* @category Internal
|
|
6
|
+
*/
|
|
7
|
+
export declare const ThemeVirContrastIndicator: import("element-vir").DeclarativeElementDefinition<"theme-vir-contrast-indicator", {
|
|
8
|
+
contrast: Readonly<CalculatedContrast>;
|
|
9
|
+
}, {}, {}, "theme-vir-contrast-indicator-", "theme-vir-contrast-indicator-", readonly []>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { classMap, css, defineElement, html, unsafeCSS } from 'element-vir';
|
|
2
|
+
import { contrastLevelLabel, ContrastLevelName, contrastLevels, } from '../contrast.js';
|
|
3
|
+
/**
|
|
4
|
+
* Show contrast details for a theme-vir color.
|
|
5
|
+
*
|
|
6
|
+
* @category Internal
|
|
7
|
+
*/
|
|
8
|
+
export const ThemeVirContrastIndicator = defineElement()({
|
|
9
|
+
tagName: 'theme-vir-contrast-indicator',
|
|
10
|
+
styles: css `
|
|
11
|
+
:host {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
max-width: 100%;
|
|
14
|
+
font-size: 12px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.wrapper {
|
|
18
|
+
text-align: center;
|
|
19
|
+
flex-grow: 1;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
max-width: 100%;
|
|
23
|
+
color: #aaa;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.${unsafeCSS(ContrastLevelName.Invisible)} {
|
|
27
|
+
color: red;
|
|
28
|
+
}
|
|
29
|
+
.${unsafeCSS(ContrastLevelName.Decoration)} {
|
|
30
|
+
color: #ff6600;
|
|
31
|
+
}
|
|
32
|
+
.${unsafeCSS(ContrastLevelName.SpotText)} {
|
|
33
|
+
color: #a5a520;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.gauge {
|
|
37
|
+
align-self: center;
|
|
38
|
+
background-color: currentColor;
|
|
39
|
+
display: flex;
|
|
40
|
+
padding: 1px;
|
|
41
|
+
gap: 1px;
|
|
42
|
+
margin-bottom: 2px;
|
|
43
|
+
/* Sure sure if I actually want to keep this. */
|
|
44
|
+
display: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.gauge-level {
|
|
48
|
+
width: 10px;
|
|
49
|
+
height: 2px;
|
|
50
|
+
|
|
51
|
+
&.active {
|
|
52
|
+
background-color: white;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.gauge-text + .gauge-text {
|
|
57
|
+
border-left: 1px solid #ccc;
|
|
58
|
+
padding-left: 1ex;
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
render({ inputs }) {
|
|
62
|
+
const gaugeLevels = contrastLevels
|
|
63
|
+
.toReversed()
|
|
64
|
+
.slice(1)
|
|
65
|
+
.map((level) => {
|
|
66
|
+
return html `
|
|
67
|
+
<div
|
|
68
|
+
class="gauge-level ${classMap({
|
|
69
|
+
active: level.min <= Math.abs(inputs.contrast.contrast),
|
|
70
|
+
})}"
|
|
71
|
+
></div>
|
|
72
|
+
`;
|
|
73
|
+
});
|
|
74
|
+
const title = [
|
|
75
|
+
inputs.contrast.contrastLevel.description,
|
|
76
|
+
'\nFont weights to font sizes:',
|
|
77
|
+
JSON.stringify(inputs.contrast.fontSizes, null, 4),
|
|
78
|
+
].join('\n');
|
|
79
|
+
const fontSize = inputs.contrast.fontSizes[400] > 150 ? '-' : `${inputs.contrast.fontSizes[400]}px`;
|
|
80
|
+
return html `
|
|
81
|
+
<div title=${title} class="wrapper ${inputs.contrast.contrastLevel.name}">
|
|
82
|
+
<div class="gauge">${gaugeLevels}</div>
|
|
83
|
+
<span>
|
|
84
|
+
<span class="gauge-text">${Math.round(inputs.contrast.contrast)} Lc</span>
|
|
85
|
+
<span class="gauge-text">
|
|
86
|
+
${contrastLevelLabel[inputs.contrast.contrastLevel.name]}
|
|
87
|
+
</span>
|
|
88
|
+
<span class="gauge-text">${fontSize}</span>
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
`;
|
|
92
|
+
},
|
|
93
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from './color/color-css.js';
|
|
2
|
+
export * from './color/color-theme-book-pages.js';
|
|
2
3
|
export * from './color/color-theme-override.js';
|
|
3
4
|
export * from './color/color-theme.js';
|
|
5
|
+
export * from './color/contrast.js';
|
|
6
|
+
export * from './color/elements/theme-vir-color-example.element.js';
|
|
7
|
+
export * from './color/elements/theme-vir-contrast-indicator.element.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
export * from './color/color-css.js';
|
|
2
|
+
export * from './color/color-theme-book-pages.js';
|
|
2
3
|
export * from './color/color-theme-override.js';
|
|
3
4
|
export * from './color/color-theme.js';
|
|
5
|
+
export * from './color/contrast.js';
|
|
6
|
+
export * from './color/elements/theme-vir-color-example.element.js';
|
|
7
|
+
export * from './color/elements/theme-vir-contrast-indicator.element.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "theme-vir",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.7.1",
|
|
4
4
|
"description": "Create an entire web theme.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"design",
|
|
@@ -43,18 +43,20 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@augment-vir/assert": "^31.20.0",
|
|
45
45
|
"@augment-vir/common": "^31.20.0",
|
|
46
|
+
"apca-w3": "^0.1.9",
|
|
46
47
|
"lit-css-vars": "^3.0.11",
|
|
47
48
|
"type-fest": "^4.41.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@augment-vir/test": "^31.20.0",
|
|
52
|
+
"@types/apca-w3": "^0.1.3",
|
|
51
53
|
"@web/dev-server-esbuild": "^1.0.4",
|
|
52
54
|
"@web/test-runner": "^0.20.2",
|
|
53
55
|
"@web/test-runner-commands": "^0.9.0",
|
|
54
56
|
"@web/test-runner-playwright": "^0.11.0",
|
|
55
57
|
"@web/test-runner-visual-regression": "^0.10.0",
|
|
56
|
-
"element-book": "^25.
|
|
57
|
-
"element-vir": "^25.
|
|
58
|
+
"element-book": "^25.7.1",
|
|
59
|
+
"element-vir": "^25.7.1",
|
|
58
60
|
"esbuild": "^0.25.4",
|
|
59
61
|
"istanbul-smart-text-reporter": "^1.1.5",
|
|
60
62
|
"markdown-code-example-inserter": "^3.0.3",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { ElementBookApp } from 'element-book';
|
|
2
|
-
import { css, defineElementNoInputs, html } from 'element-vir';
|
|
3
|
-
import { createTheme } from '../create-theme/create-theme.js';
|
|
4
|
-
import { createThemeBookPages } from './theme-book-pages.js';
|
|
5
|
-
export const VirThemeBookApp = defineElementNoInputs({
|
|
6
|
-
tagName: 'vir-theme-book-app',
|
|
7
|
-
styles: css `
|
|
8
|
-
:host {
|
|
9
|
-
display: flex;
|
|
10
|
-
flex-direction: column;
|
|
11
|
-
gap: 32px;
|
|
12
|
-
min-height: 100%;
|
|
13
|
-
width: 100%;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
${ElementBookApp} {
|
|
17
|
-
flex-grow: 1;
|
|
18
|
-
}
|
|
19
|
-
`,
|
|
20
|
-
state() {
|
|
21
|
-
return {
|
|
22
|
-
theme: createTheme({
|
|
23
|
-
elementTagPrefix: 'vir',
|
|
24
|
-
}),
|
|
25
|
-
};
|
|
26
|
-
},
|
|
27
|
-
render({ state }) {
|
|
28
|
-
return html `
|
|
29
|
-
<${ElementBookApp.assign({
|
|
30
|
-
pages: createThemeBookPages(state.theme),
|
|
31
|
-
})}></${ElementBookApp}>
|
|
32
|
-
`;
|
|
33
|
-
},
|
|
34
|
-
});
|