theme-vir 28.13.0 → 28.15.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # theme-vir
2
2
 
3
- Easy-to-create, all-in-one theme generator with `element-vir`. WIP.
3
+ Easy-to-create, all-in-one theme generator with [`element-vir`](https://npmjs.com/package/element-vir). WIP.
4
4
 
5
- Reference docs: https://electrovir.github.io/vira/theme-vir/docs
5
+ Reference docs: https://electrovir.github.io/theme-vir
@@ -0,0 +1,85 @@
1
+ import { type PartialWithUndefined, type RequiredAndNotNull } from '@augment-vir/common';
2
+ import { type CssVarDefinitions, type SingleCssVarDefinition } from 'lit-css-vars';
3
+ import { type NoRefColorInit } from './color-theme.js';
4
+ import { ContrastLevelName } from './contrast.js';
5
+ /** @category Internal */
6
+ export type ColorPaletteVars = CssVarDefinitions<Record<`${string}-${string}-${number}`, any>>;
7
+ /** @category Internal */
8
+ export type PaletteColor = {
9
+ suffix: string | undefined;
10
+ prefix: string;
11
+ colorName: string;
12
+ definition: SingleCssVarDefinition;
13
+ cssVarName: string;
14
+ };
15
+ /** @category Internal */
16
+ export type ColorGroups = Record<string, PaletteColor[]>;
17
+ /**
18
+ * Black and white color values.
19
+ *
20
+ * @category Internal
21
+ */
22
+ export declare const defaultOmittedColorGroupColorValues: string[];
23
+ /** @category Internal */
24
+ export declare function groupColors(colors: Readonly<ColorPaletteVars>,
25
+ /**
26
+ * Color values to omit from the grouping. Defaults to
27
+ * {@link defaultOmittedColorGroupColorValues}.
28
+ *
29
+ * @default defaultOmittedColorGroupColorValues
30
+ */
31
+ omittedColorValues?: ReadonlyArray<string>): ColorGroups;
32
+ /** @category Internal */
33
+ export declare function extractPaletteColor(color: Readonly<SingleCssVarDefinition>): PaletteColor;
34
+ /** @category Internal */
35
+ export declare function extractParam<const T extends PropertyKey>(possibleParams: ReadonlyArray<PropertyKey> | Readonly<Record<PropertyKey, boolean>>, { mapFrom, mapTo, }: Readonly<PartialWithUndefined<{
36
+ mapTo: Record<string, T>;
37
+ mapFrom: Record<T, any>;
38
+ }>>): T[];
39
+ /** @category Internal */
40
+ export type ArrayOrSelectParam<T extends PropertyKey> = ReadonlyArray<T> | Readonly<Partial<Record<T, boolean>>>;
41
+ /** @category Internal */
42
+ export declare const defaultLightThemePair: RequiredAndNotNull<NoRefColorInit>;
43
+ /** @category Internal */
44
+ export declare const defaultContrastLevels: Readonly<ArrayOrSelectParam<ContrastLevelName>>;
45
+ /**
46
+ * Creates a color theme from a color palette.
47
+ *
48
+ * @category Color Theme
49
+ */
50
+ export declare function buildLowLevelColorTheme(colorPalette: Readonly<ColorPaletteVars>, { defaultTheme, omittedColorValues, crossContrastLevels, }?: Readonly<PartialWithUndefined<{
51
+ /**
52
+ * The default theme colors for {@link defineColorTheme}. Defaults to
53
+ * {@link defaultLightThemePair}.
54
+ *
55
+ * @default defaultLightThemePair
56
+ */
57
+ defaultTheme: RequiredAndNotNull<NoRefColorInit>;
58
+ /**
59
+ * All font weights to cross colors with. Defaults to {@link defaultContrastLevels}.
60
+ *
61
+ * @default defaultContrastLevels
62
+ */
63
+ crossContrastLevels: Readonly<ArrayOrSelectParam<ContrastLevelName>>;
64
+ /**
65
+ * Color values to omit from the grouping. Defaults to
66
+ * {@link defaultOmittedColorGroupColorValues}.
67
+ *
68
+ * @default defaultOmittedColorGroupColorValues
69
+ */
70
+ omittedColorValues: ReadonlyArray<string>;
71
+ }>>): import("./color-theme.js").ColorTheme<{
72
+ [k: string]: ((Required<Pick<{
73
+ foreground: import("./color-theme.js").ColorInitValue;
74
+ background: import("./color-theme.js").ColorInitValue;
75
+ }, "foreground">> & Partial<Pick<{
76
+ foreground: import("./color-theme.js").ColorInitValue;
77
+ background: import("./color-theme.js").ColorInitValue;
78
+ }, "background">>) | (Required<Pick<{
79
+ foreground: import("./color-theme.js").ColorInitValue;
80
+ background: import("./color-theme.js").ColorInitValue;
81
+ }, "background">> & Partial<Pick<{
82
+ foreground: import("./color-theme.js").ColorInitValue;
83
+ background: import("./color-theme.js").ColorInitValue;
84
+ }, "foreground">>)) & {};
85
+ }>;
@@ -0,0 +1,183 @@
1
+ import { assert, assertWrap, check } from '@augment-vir/assert';
2
+ import { crossProduct, filterMap, getOrSet, log, mapObjectValues, removeDuplicates, stringify, } from '@augment-vir/common';
3
+ import { defineColorTheme, noRefColorInitToString, } from './color-theme.js';
4
+ import { contrastLevelLabel, ContrastLevelName, findClosestColor, findColorAtContrastLevel, } from './contrast.js';
5
+ /**
6
+ * Black and white color values.
7
+ *
8
+ * @category Internal
9
+ */
10
+ export const defaultOmittedColorGroupColorValues = [
11
+ '#000000',
12
+ '#ffffff',
13
+ '#000',
14
+ '#fff',
15
+ 'white',
16
+ 'black',
17
+ ];
18
+ /** @category Internal */
19
+ export function groupColors(colors,
20
+ /**
21
+ * Color values to omit from the grouping. Defaults to
22
+ * {@link defaultOmittedColorGroupColorValues}.
23
+ *
24
+ * @default defaultOmittedColorGroupColorValues
25
+ */
26
+ omittedColorValues = defaultOmittedColorGroupColorValues) {
27
+ const colorGroups = {};
28
+ Object.values(colors).forEach((color) => {
29
+ if (omittedColorValues.includes(color.default)) {
30
+ return;
31
+ }
32
+ const paletteColor = extractPaletteColor(color);
33
+ getOrSet(colorGroups, paletteColor.colorName, () => []).push(paletteColor);
34
+ });
35
+ return colorGroups;
36
+ }
37
+ /** @category Internal */
38
+ export function extractPaletteColor(color) {
39
+ const split = String(color.name).replace(/^-+/, '').split('-');
40
+ const suffix = split.length > 2 ? split.at(-1) : undefined;
41
+ const prefix = assertWrap.isTruthy(split[0]);
42
+ // eslint-disable-next-line sonarjs/argument-type
43
+ const colorName = split.slice(1, suffix ? -1 : undefined).join('-');
44
+ return {
45
+ suffix,
46
+ prefix,
47
+ colorName,
48
+ definition: color,
49
+ cssVarName: String(color.name),
50
+ };
51
+ }
52
+ /** @category Internal */
53
+ export function extractParam(possibleParams, { mapFrom, mapTo, }) {
54
+ if (check.isArray(possibleParams)) {
55
+ return removeDuplicates(possibleParams.map((param) => {
56
+ if (mapFrom && check.isKeyOf(param, mapFrom)) {
57
+ return param;
58
+ }
59
+ else if (mapTo && check.isKeyOf(param, mapTo) && mapTo[param] != undefined) {
60
+ return mapTo[param];
61
+ }
62
+ else {
63
+ throw new Error(`Unknown font weight: ${String(param)}`);
64
+ }
65
+ }));
66
+ }
67
+ else {
68
+ return extractParam(filterMap(Object.entries(possibleParams), ([name, enabled,]) => {
69
+ if (enabled) {
70
+ /**
71
+ * This cast is okay because the recursive case (handling an array) will
72
+ * guard against bas names or weights.
73
+ */
74
+ return name;
75
+ }
76
+ else {
77
+ return undefined;
78
+ }
79
+ }, check.isTruthy), {
80
+ mapTo,
81
+ mapFrom,
82
+ });
83
+ }
84
+ }
85
+ /** @category Internal */
86
+ export const defaultLightThemePair = {
87
+ background: 'white',
88
+ foreground: 'black',
89
+ };
90
+ /** @category Internal */
91
+ export const defaultContrastLevels = {
92
+ [ContrastLevelName.BodyText]: true,
93
+ [ContrastLevelName.Header]: true,
94
+ [ContrastLevelName.Placeholder]: true,
95
+ [ContrastLevelName.Decoration]: true,
96
+ };
97
+ /**
98
+ * Creates a color theme from a color palette.
99
+ *
100
+ * @category Color Theme
101
+ */
102
+ export function buildLowLevelColorTheme(colorPalette, { defaultTheme = defaultLightThemePair, omittedColorValues = defaultOmittedColorGroupColorValues, crossContrastLevels = defaultContrastLevels, } = {}) {
103
+ const contrastLevels = extractParam(crossContrastLevels, {
104
+ mapFrom: contrastLevelLabel,
105
+ });
106
+ const colorGroups = groupColors(colorPalette, omittedColorValues);
107
+ const themeColors = Object.fromEntries(Object.entries(colorGroups).flatMap(([colorGroupName, colors,]) => {
108
+ assert.isLengthAtLeast(colors, 1);
109
+ const colorStrings = colors.map((color) => color.definition.default);
110
+ const allCrosses = crossProduct({
111
+ crossWith: [
112
+ 'ahead-background',
113
+ 'behind-background',
114
+ 'ahead-foreground',
115
+ 'behind-foreground',
116
+ 'self-light-front',
117
+ 'self-light-back',
118
+ ],
119
+ contrast: contrastLevels,
120
+ // fontWeight: fontWeights,
121
+ });
122
+ const firstColor = colors[0];
123
+ const defaultForegroundString = noRefColorInitToString(defaultTheme.foreground);
124
+ const defaultBackgroundString = noRefColorInitToString(defaultTheme.background);
125
+ const lightestSelf = findClosestColor('white', colorStrings);
126
+ return filterMap(allCrosses, (cross) => {
127
+ const crossName = [
128
+ firstColor.prefix,
129
+ firstColor.colorName,
130
+ cross.crossWith,
131
+ cross.contrast,
132
+ // cross.fontWeight,
133
+ ].join('-');
134
+ const comparison = cross.crossWith === 'ahead-background'
135
+ ? {
136
+ foreground: colorStrings,
137
+ background: defaultBackgroundString,
138
+ }
139
+ : cross.crossWith === 'behind-background'
140
+ ? {
141
+ foreground: defaultBackgroundString,
142
+ background: colorStrings,
143
+ }
144
+ : cross.crossWith === 'ahead-foreground'
145
+ ? {
146
+ foreground: colorStrings,
147
+ background: defaultForegroundString,
148
+ }
149
+ : cross.crossWith === 'behind-foreground'
150
+ ? {
151
+ foreground: defaultForegroundString,
152
+ background: colorStrings,
153
+ }
154
+ : cross.crossWith === 'self-light-back'
155
+ ? {
156
+ foreground: colorStrings,
157
+ background: lightestSelf,
158
+ }
159
+ : {
160
+ foreground: lightestSelf,
161
+ background: colorStrings,
162
+ };
163
+ const matchedColorString = findColorAtContrastLevel(comparison, cross.contrast);
164
+ const matchedColor = colors.find((color) => color.definition.default === matchedColorString);
165
+ if (!matchedColor) {
166
+ log.error(`No valid '${colorGroupName}' color cross found for: ${stringify(cross)} with ${stringify(colorStrings)}`);
167
+ return undefined;
168
+ }
169
+ return [
170
+ crossName,
171
+ mapObjectValues(comparison, (key, value) => {
172
+ if (check.isString(value)) {
173
+ return value;
174
+ }
175
+ else {
176
+ return matchedColor.definition.value;
177
+ }
178
+ }),
179
+ ];
180
+ }, check.isTruthy);
181
+ }));
182
+ return defineColorTheme(defaultTheme, themeColors);
183
+ }
@@ -0,0 +1,18 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ import { type BookPage } from 'element-book';
3
+ import { type ColorPaletteVars } from './build-color-theme.js';
4
+ /**
5
+ * Create multiple element-book pages to showcase a bunch of color CSS variables.
6
+ *
7
+ * @category Color Theme
8
+ * @see `createColorThemeBookPages` for creating full color theme pages.
9
+ */
10
+ export declare function createColorPaletteBookPages({ colors, parent, title, includeContrast, includeTheme, useVerticalTheme, }: {
11
+ parent: Readonly<BookPage>;
12
+ title: string;
13
+ colors: Readonly<ColorPaletteVars>;
14
+ } & PartialWithUndefined<{
15
+ includeContrast: boolean;
16
+ includeTheme: boolean;
17
+ useVerticalTheme: boolean;
18
+ }>): BookPage[];
@@ -0,0 +1,232 @@
1
+ import { check } from '@augment-vir/assert';
2
+ import { defineBookPage } from 'element-book';
3
+ import { css, html, unsafeCSS } from 'element-vir';
4
+ import { noNativeSpacing, viraColorPalette } from 'vira';
5
+ import { buildLowLevelColorTheme, groupColors, } from './build-color-theme.js';
6
+ import { createColorThemeBookPages } from './color-theme-book-pages.js';
7
+ import { ThemeVirColorExample } from './elements/theme-vir-color-example.element.js';
8
+ const blackWhiteCells = [
9
+ {
10
+ title: 'Black',
11
+ fontWeight: 400,
12
+ foreground: viraColorPalette['vira-black'],
13
+ },
14
+ {
15
+ title: 'Black',
16
+ fontWeight: 700,
17
+ foreground: viraColorPalette['vira-black'],
18
+ },
19
+ {
20
+ title: 'White',
21
+ fontWeight: 400,
22
+ foreground: viraColorPalette['vira-white'],
23
+ },
24
+ {
25
+ title: 'White',
26
+ fontWeight: 700,
27
+ foreground: viraColorPalette['vira-white'],
28
+ },
29
+ {
30
+ title: 'Black',
31
+ fontWeight: 400,
32
+ background: viraColorPalette['vira-black'],
33
+ },
34
+ {
35
+ title: 'Black',
36
+ fontWeight: 700,
37
+ background: viraColorPalette['vira-black'],
38
+ },
39
+ {
40
+ title: 'White',
41
+ fontWeight: 400,
42
+ background: viraColorPalette['vira-white'],
43
+ },
44
+ {
45
+ title: 'White',
46
+ fontWeight: 700,
47
+ background: viraColorPalette['vira-white'],
48
+ },
49
+ ];
50
+ /**
51
+ * Create multiple element-book pages to showcase a bunch of color CSS variables.
52
+ *
53
+ * @category Color Theme
54
+ * @see `createColorThemeBookPages` for creating full color theme pages.
55
+ */
56
+ export function createColorPaletteBookPages({ colors, parent, title, includeContrast, includeTheme, useVerticalTheme, }) {
57
+ const colorGroups = groupColors(colors);
58
+ const topColorsPage = defineBookPage({
59
+ parent,
60
+ title,
61
+ });
62
+ const colorPalettePage = defineBookPage({
63
+ parent: topColorsPage,
64
+ title: 'Palette',
65
+ defineExamples({ defineExample }) {
66
+ Object.entries(colorGroups).forEach(([groupName, colors,]) => {
67
+ defineExample({
68
+ title: groupName,
69
+ styles: css `
70
+ :host {
71
+ display: flex;
72
+ flex-direction: column;
73
+ }
74
+
75
+ .swatch-wrapper {
76
+ display: flex;
77
+ gap: 4px;
78
+ align-items: center;
79
+
80
+ & .swatch {
81
+ width: 50px;
82
+ height: 50px;
83
+ }
84
+
85
+ & .color-details {
86
+ font-family: monospace;
87
+ font-size: 12px;
88
+ color: ${viraColorPalette['vira-grey-50'].value};
89
+ }
90
+
91
+ & .color-value {
92
+ margin-left: 1ch;
93
+ }
94
+ }
95
+ `,
96
+ render() {
97
+ return colors.map((color) => {
98
+ return html `
99
+ <div class="swatch-wrapper">
100
+ <div
101
+ class="swatch"
102
+ style=${css `
103
+ background-color: ${unsafeCSS(color.definition.default)};
104
+ `}
105
+ ></div>
106
+ <p class="color-details">
107
+ <span>${color.cssVarName}</span>
108
+ <br />
109
+ <span class="color-value">
110
+ ${color.definition.default}
111
+ </span>
112
+ </p>
113
+ </div>
114
+ `;
115
+ });
116
+ },
117
+ });
118
+ });
119
+ },
120
+ });
121
+ const contrastsPage = defineBookPage({
122
+ parent: topColorsPage,
123
+ title: 'Palette Contrast',
124
+ });
125
+ function createContrastPage(contrastPageTitle, contrastCellsInput) {
126
+ return defineBookPage({
127
+ parent: contrastsPage,
128
+ title: `${title} ${contrastPageTitle}`,
129
+ defineExamples({ defineExample }) {
130
+ Object.entries(colorGroups).forEach(([groupName, colors,]) => {
131
+ const contrastCells = check.isArray(contrastCellsInput)
132
+ ? contrastCellsInput
133
+ : contrastCellsInput(colors);
134
+ defineExample({
135
+ title: groupName,
136
+ styles: css `
137
+ :host {
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 24px;
141
+ }
142
+
143
+ p {
144
+ ${noNativeSpacing}
145
+ }
146
+
147
+ .darkness-level {
148
+ text-align: center;
149
+ font-size: 12px;
150
+ color: ${viraColorPalette['vira-grey-50'].value};
151
+ }
152
+
153
+ td {
154
+ padding: 4px;
155
+ min-width: 170px;
156
+ }
157
+ `,
158
+ render() {
159
+ const colorRowTemplates = colors.map((color) => {
160
+ const cellTemplates = contrastCells.map((cell) => {
161
+ return html `
162
+ <td>
163
+ <p class="darkness-level">${color.suffix}</p>
164
+ <${ThemeVirColorExample.assign({
165
+ color: {
166
+ background: cell.background || color.definition,
167
+ foreground: cell.foreground || color.definition,
168
+ },
169
+ showVarValues: true,
170
+ showVarNames: false,
171
+ showContrast: true,
172
+ fontWeight: cell.fontWeight,
173
+ })}></${ThemeVirColorExample}>
174
+ </td>
175
+ `;
176
+ });
177
+ return html `
178
+ <tr>${cellTemplates}</tr>
179
+ `;
180
+ });
181
+ const headerCells = contrastCells.map((cell) => {
182
+ const layerText = cell.background ? 'in back' : 'in front';
183
+ const title = [
184
+ cell.title,
185
+ `(${layerText})`,
186
+ `(${cell.fontWeight})`,
187
+ ].join(' ');
188
+ return html `
189
+ <th>${title}</th>
190
+ `;
191
+ });
192
+ return html `
193
+ <table cellspacing="0" cellpadding="0">
194
+ <thead><tr>${headerCells}</tr></thead>
195
+ <tbody>${colorRowTemplates}</tbody>
196
+ </table>
197
+ `;
198
+ },
199
+ });
200
+ });
201
+ },
202
+ });
203
+ }
204
+ const blackWhiteContrastPage = createContrastPage('Contrast Black White', blackWhiteCells);
205
+ function createSelfContrastPage(fontWeight) {
206
+ return createContrastPage(`Contrast Self ${fontWeight}`, (colors) => colors.map((color) => {
207
+ return {
208
+ fontWeight,
209
+ title: color.suffix || '',
210
+ foreground: color.definition,
211
+ };
212
+ }));
213
+ }
214
+ return [
215
+ topColorsPage,
216
+ colorPalettePage,
217
+ contrastsPage,
218
+ includeContrast ? blackWhiteContrastPage : undefined,
219
+ includeContrast ? createSelfContrastPage(400) : undefined,
220
+ includeContrast ? createSelfContrastPage(700) : undefined,
221
+ ...(includeTheme
222
+ ? createColorThemeBookPages({
223
+ parent: topColorsPage,
224
+ title: 'Theme (auto)',
225
+ theme: buildLowLevelColorTheme(colors),
226
+ hideInverseColors: true,
227
+ useVerticalLayout: useVerticalTheme,
228
+ prefixGroupByCount: 2,
229
+ })
230
+ : []),
231
+ ].filter(check.isTruthy);
232
+ }
@@ -7,17 +7,24 @@ import { type ColorTheme } from './color-theme.js';
7
7
  *
8
8
  * @category Color Theme
9
9
  */
10
- export declare function createColorThemeBookPages({ parent, title, theme, hideInverseColors, overrides, }: {
10
+ export declare function createColorThemeBookPages({ parent, title, theme, hideInverseColors, overrides, useVerticalLayout, prefixGroupByCount, }: {
11
11
  title: string;
12
12
  theme: Readonly<ColorTheme>;
13
13
  } & PartialWithUndefined<{
14
14
  parent: Readonly<BookPage>;
15
15
  hideInverseColors: boolean;
16
16
  overrides: ReadonlyArray<Readonly<ColorThemeOverride>>;
17
+ useVerticalLayout: boolean;
18
+ /**
19
+ * The number of CSS variable name prefixes to group colors by.
20
+ *
21
+ * @default 0
22
+ */
23
+ prefixGroupByCount: number;
17
24
  }>): (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>;
25
+ 'Show Var Names': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
26
+ 'Show Contrast Tips': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
20
27
  }> | 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>;
28
+ 'Show Var Names': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
29
+ 'Show Contrast Tips': import("element-book").BookPageControlInit<BookPageControlType.Checkbox>;
23
30
  }>, {}>)[];