@wistia/kaleidoscope 0.0.1 → 0.0.2-beta.28ee13df.cba4a37

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,6 +1,6 @@
1
1
  # @wistia/kaleidoscope
2
2
 
3
- TODO: describe what this package does.
3
+ Shared color logic for Wistia's audience-facing design system, Kaleidoscope.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,6 +8,96 @@ TODO: describe what this package does.
8
8
  yarn add @wistia/kaleidoscope
9
9
  ```
10
10
 
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { Color, ColorScale } from '@wistia/kaleidoscope';
15
+
16
+ const brand = new Color('#2949e5');
17
+
18
+ brand.lighten(0.5); // Lighten by 50%, preserving hue
19
+ brand.blend('#ffcc00', 0.5); // Blend yellow into base color, using oklch color space
20
+ brand.contrast('#ffffff'); // fetch the contrast ratio of the given color against white
21
+
22
+ // Yields a set of 5 colors between black, brand blue, and white, where the lightness is
23
+ // adjusted for optical balance throughout the scale.
24
+ new ColorScale(['#000000', brand, '#ffffff']).correctLightness().colors(5);
25
+ ```
26
+
27
+ `Color` reads hex (`#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, with or without the `#`),
28
+ `rgb()`/`rgba()` in both legacy and modern CSS syntax, `[r, g, b, a]` tuples, and any
29
+ `{ r, g, b, a }` object. Named CSS colors, e.g., "darkcyan", are not supported.
30
+
31
+ Every method returns a new instance, so a color or scale can be shared without anything
32
+ mutating it out from under its other holders.
33
+
34
+ | `Color` | |
35
+ | --- | --- |
36
+ | **Conversion** | `toHex`, `toHexWithAlpha`, `toRgb`, `toRgba`, `toRgbaTuple`, `toHsl`, `toLab`, `toString` |
37
+ | **Adjust lightness** | `lighten`, `darken`, `withLightness` |
38
+ | **Adjust mix** | `tint`, `shade`, `blend` |
39
+ | **Adjust alpha** | `withAlpha` |
40
+ | **Measure** | `luminance`, `contrast` |
41
+ | **Construct** | `Color.parse`, `Color.fromHsl`, `Color.fromLab` |
42
+
43
+ | `ColorScale` | |
44
+ | --- | --- |
45
+ | **Generate** | `at`, `colors` |
46
+ | **Configure** | `mode`, `domain`, `gamma`, `padding`, `classes`, `correctLightness` |
47
+
48
+
49
+ ## Contrast
50
+
51
+ Two helpers pick or repair a color so it clears the WCAG ratio its content needs. Both
52
+ take named options and return a `Color`; `shape` names the kind of content and defaults to
53
+ `paragraphText` (4.5:1).
54
+
55
+ ```ts
56
+ import { adjustColorForProperContrast, getBestColorForText } from '@wistia/kaleidoscope';
57
+
58
+ // Choose from a palette. Of the candidates that clear the ratio, you get the one closest
59
+ // in tone to the background — legible without jumping to stark black or white.
60
+ getBestColorForText({
61
+ backgroundColor: '#ffffff',
62
+ palette: ['#2949e5', '#c7d3ff'],
63
+ shape: 'smallText',
64
+ }); // '#2949e5' — the tint is too pale to clear 5.5:1
65
+
66
+ // Repair a single color. Hue is pinned, so the result still reads as the color you gave it.
67
+ adjustColorForProperContrast({
68
+ foregroundColor: '#2949e5',
69
+ backgroundColor: '#2949e5',
70
+ shape: 'nonText',
71
+ });
72
+ ```
73
+
74
+ `getBestColorForText` always keeps black and white in play, so an empty `palette` still
75
+ yields whichever of the two reads against the background, and `exclude` can never strand it
76
+ without a legible option. It also keeps text on the side it belongs on — light on a dark
77
+ background, dark on a light one — since the WCAG ratio is symmetric and would otherwise
78
+ allow murky dark-on-dark results at the looser ratios. `adjustColorForProperContrast`
79
+ returns its input untouched when that already contrasts.
80
+
81
+ Some backgrounds cannot reach the stricter ratios from any color at all — a mid-gray tops
82
+ out near 5.3:1, short of `smallText`. Rather than throwing, both helpers then return the
83
+ most contrasting color available, which is always black or white and is picked by measuring
84
+ both. Check the result with `contrast` when the ratio is load-bearing.
85
+
86
+
87
+ ## Storybook
88
+
89
+ Color is easier to judge by eye than by assertion, so every feature has a story, including
90
+ playgrounds for `Color` and `ColorScale` wired to live controls.
91
+
92
+ **[Browse the Storybook →](https://wistia.github.io/vhs/storybook-kaleidoscope/)**
93
+
94
+ Or run it locally, from the repo root:
95
+
96
+ ```sh
97
+ yarn storybook:kaleidoscope # serves on http://127.0.0.1:6008
98
+ ```
99
+
100
+
11
101
  ## Development
12
102
 
13
103
  This package lives in the [vhs monorepo](https://github.com/wistia/vhs). From the repo root:
package/dist/index.d.ts CHANGED
@@ -1,11 +1,280 @@
1
1
 
2
2
  //#region src/colorContrastRatiosByShape.d.ts
3
3
  declare const colorContrastRatiosByShape: {
4
- nonText: number;
5
- largeText: number;
6
- paragraphText: number;
7
- smallText: number;
4
+ readonly nonText: 3.0;
5
+ readonly largeText: 3.0;
6
+ readonly paragraphText: 4.5;
7
+ readonly smallText: 5.5;
8
8
  };
9
+ /** The kind of content a color has to be legible as, which sets the ratio it has to clear. */
10
+ type ColorShape = keyof typeof colorContrastRatiosByShape;
9
11
  //#endregion
10
- export { colorContrastRatiosByShape };
12
+ //#region src/Color.d.ts
13
+ /**
14
+ * An immutable sRGB color with transformation methods like lighten, darken, tint, shade.
15
+ * Also includes methods to calculate contrast ratio against another color, relative luminance, etc.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * new Color('#2949e5').lighten(0.5).toHex(); // '#89aaff'
20
+ * new Color('#2949e5').withAlpha(0.5).toRgba(); // 'rgba(41, 73, 229, 0.5)'
21
+ * new Color('#2949e5').contrast('#ffffff'); // 6.6
22
+ * ```
23
+ */
24
+ declare class Color {
25
+ #private;
26
+ /**
27
+ * Read `input` into a color, returning `null` when it cannot be parsed.
28
+ */
29
+ static parse(input: ColorInput): Color | null;
30
+ static fromHsl(hsl: Hsl, alpha?: number): Color;
31
+ static fromLab(lab: Lab, alpha?: number): Color;
32
+ /** Red channel, 0–255. Kept unrounded so that chained operations don't accumulate error. */
33
+ readonly r: number;
34
+ /** Green channel, 0–255. Kept unrounded so that chained operations don't accumulate error. */
35
+ readonly g: number;
36
+ /** Blue channel, 0–255. Kept unrounded so that chained operations don't accumulate error. */
37
+ readonly b: number;
38
+ /** Alpha channel, 0–1. */
39
+ readonly a: number;
40
+ /**
41
+ * @param input - any supported color representation. Input that cannot be read as a
42
+ * color resolves to brand blue rather than failing, so a malformed value degrades to
43
+ * a usable color instead of interrupting a render.
44
+ */
45
+ constructor(input: ColorInput);
46
+ /** `#rrggbb`. Alpha is dropped — use {@link Color.toHexWithAlpha} to keep it. */
47
+ toHex(): string;
48
+ /** `#rrggbbaa`. */
49
+ toHexWithAlpha(): string;
50
+ /** `rgb(41, 73, 229)`. */
51
+ toRgb(): string;
52
+ /** `rgba(41, 73, 229, 0.5)`. */
53
+ toRgba(): string;
54
+ /** Rounded channels, ready to hand to another color library. */
55
+ toRgbaTuple(): RgbaTuple;
56
+ toHsl(): Hsl;
57
+ toLab(): Lab;
58
+ /** Hex when fully opaque, `rgba()` otherwise, so the alpha is never silently lost. */
59
+ toString(): string;
60
+ /**
61
+ * WCAG relative luminance, from 0 (black) to 1 (white). Alpha is ignored — luminance
62
+ * is only meaningful once a color has been composited onto a background.
63
+ */
64
+ luminance(): number;
65
+ /**
66
+ * WCAG contrast ratio against `other`, from 1 (identical) to 21 (black on white),
67
+ * reported to one decimal place.
68
+ *
69
+ * @see {@link colorContrastRatiosByShape} for the ratio each kind of content needs
70
+ */
71
+ contrast(other: ColorInput): number;
72
+ /**
73
+ * Raise perceptual lightness `ratio` (0–1) of the way to white, holding the hue steady.
74
+ *
75
+ * The hue is held in OKLCh, so a lightened color stays recognizably itself: a blue
76
+ * lightens to a paler blue instead of drifting purple, and a red that runs out of sRGB
77
+ * gives up colorfulness rather than sliding towards orange. That is the difference from
78
+ * {@link Color.tint}, which stirs white into the channels and lets the hue land where it
79
+ * may.
80
+ */
81
+ lighten(ratio: number): Color;
82
+ /** Lower perceptual lightness `ratio` (0–1) of the way to black, holding the hue steady. */
83
+ darken(ratio: number): Color;
84
+ /** Mix `ratio` (0–1) of white in. */
85
+ tint(ratio: number, mode?: InterpolationMode): Color;
86
+ /** Mix `ratio` (0–1) of black in. */
87
+ shade(ratio: number, mode?: InterpolationMode): Color;
88
+ /**
89
+ * Mix towards `other`, where `ratio` 0 keeps this color and 1 returns `other`. Alpha is
90
+ * always interpolated linearly, whichever `mode` the channels travel through.
91
+ */
92
+ blend(other: ColorInput, ratio?: number, mode?: InterpolationMode): Color;
93
+ /** Replace the alpha channel with `alpha` (0–1). */
94
+ withAlpha(alpha: number): Color;
95
+ /**
96
+ * Replace HSL lightness with `lightness` (0–100), leaving hue and saturation alone.
97
+ * Use this to hit an absolute lightness; use {@link Color.lighten} to step relative to
98
+ * where the color already is.
99
+ */
100
+ withLightness(lightness: number): Color;
101
+ }
102
+ //#endregion
103
+ //#region src/types.d.ts
104
+ /** Red, green and blue channels, each in the 0–255 range. */
105
+ type RgbTuple = [red: number, green: number, blue: number];
106
+ /** Red, green and blue channels (0–255) plus an alpha channel (0–1). */
107
+ type RgbaTuple = [red: number, green: number, blue: number, alpha: number];
108
+ /** RGB channels (0–255) with an optional alpha channel (0–1). */
109
+ type ColorTuple = [red: number, green: number, blue: number, alpha?: number];
110
+ /** Any object carrying RGB channels (0–255) and an optional alpha channel (0–1). */
111
+ type RgbaObject = {
112
+ readonly a?: number;
113
+ readonly b: number;
114
+ readonly g: number;
115
+ readonly r: number;
116
+ };
117
+ /**
118
+ * Anything that can be read as a color:
119
+ *
120
+ * - a hex string — `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa` (the `#` is optional)
121
+ * - an `rgb()`/`rgba()` string in either legacy comma syntax (`rgb(41, 73, 229)`) or
122
+ * modern space syntax (`rgb(41 73 229 / 50%)`), with numbers or percentages
123
+ * - an `[r, g, b]` or `[r, g, b, a]` tuple
124
+ * - an object with `r`, `g`, `b` and optionally `a` — including another `Color`
125
+ *
126
+ * CSS named colors (`red`, `transparent`, …) are not supported.
127
+ */
128
+ type ColorInput = Color | ColorTuple | RgbaObject | string;
129
+ /** Hue (0–360), saturation (0–100) and lightness (0–100). */
130
+ type Hsl = {
131
+ readonly hue: number;
132
+ readonly lightness: number;
133
+ readonly saturation: number;
134
+ };
135
+ /**
136
+ * CIELAB: perceptual lightness (0–100), plus the green–red (`a`) and blue–yellow (`b`)
137
+ * axes, which are unbounded but in practice fall within ±128.
138
+ */
139
+ type Lab = {
140
+ readonly a: number;
141
+ readonly b: number;
142
+ readonly lightness: number;
143
+ };
144
+ /**
145
+ * The color space a blend interpolates through.
146
+ *
147
+ * - `oklch` — perceptually uniform, and the only mode that carries hue as a hue, so a
148
+ * blend takes the short way round the wheel instead of through the grays in between
149
+ * - `lab` — perceptually uniform, interpolated on the rectangular axes
150
+ * - `lrgb` — linear RGB; avoids the muddy midpoints of naive RGB blending
151
+ * - `rgb` — naive channel-wise RGB; fastest, and what CSS `color-mix()` does in sRGB
152
+ */
153
+ type InterpolationMode = "lab" | "lrgb" | "oklch" | "rgb";
154
+ //#endregion
155
+ //#region src/adjustColorForProperContrast.d.ts
156
+ type AdjustColorForProperContrastOptions = {
157
+ /** What the foreground sits on. Never adjusted. */
158
+ readonly backgroundColor: ColorInput;
159
+ /** The color to nudge until it is legible. Returned untouched if it already is. */
160
+ readonly foregroundColor: ColorInput;
161
+ /** What the foreground is. Defaults to `paragraphText` (4.5:1). */
162
+ readonly shape?: ColorShape;
163
+ };
164
+ /**
165
+ * Walk `foregroundColor` towards white or black until it clears the contrast ratio
166
+ * `shape` requires against `backgroundColor`.
167
+ *
168
+ * Hue is pinned for the whole walk, so the result still reads as the color you passed in
169
+ * — a washed-out brand blue stays blue rather than drifting towards its neighbors. The
170
+ * direction is whichever gains contrast faster, reversing if it hits an extreme with the
171
+ * ratio still unmet. Text-grade ratios that can't be met either way settle on plain white
172
+ * or black, since at that point nothing else beats them.
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * // Already legible, so nothing moves.
177
+ * adjustColorForProperContrast({
178
+ * foregroundColor: '#2949e5',
179
+ * backgroundColor: '#ffffff',
180
+ * }).toHex(); // '#2949e5'
181
+ *
182
+ * // A brand blue on itself, darkened until it separates.
183
+ * adjustColorForProperContrast({
184
+ * foregroundColor: '#2949e5',
185
+ * backgroundColor: '#2949e5',
186
+ * shape: 'nonText',
187
+ * }).toHex();
188
+ * ```
189
+ */
190
+ declare const adjustColorForProperContrast: ({ backgroundColor, foregroundColor, shape }: AdjustColorForProperContrastOptions) => Color;
191
+ //#endregion
192
+ //#region src/ColorScale.d.ts
193
+ type ColorScaleOptions = {
194
+ /**
195
+ * Quantize the scale into bands instead of leaving it continuous: a count of equally sized
196
+ * classes across the domain, or explicit class breaks.
197
+ */
198
+ readonly classes?: number | number[];
199
+ /**
200
+ * Redistribute stops so that perceived lightness moves evenly from end to end. Worth
201
+ * turning on for any scale a reader is meant to rank by eye.
202
+ */
203
+ readonly correctLightness?: boolean;
204
+ /**
205
+ * The input range, `[0, 1]` by default. Two values set the ends; supply one value per
206
+ * color to pin each color to a position, or more than two to warp the scale.
207
+ */
208
+ readonly domain?: number[];
209
+ /** Bias the scale towards one end. Above 1 favors the start, below 1 the end. */
210
+ readonly gamma?: number;
211
+ /** The color space blends travel through. */
212
+ readonly mode?: InterpolationMode;
213
+ /** Trim `[start, end]` fractions off the scale, dropping its most extreme colors. */
214
+ readonly padding?: [number, number] | number;
215
+ };
216
+ /**
217
+ * A continuous scale between two or more colors — the chroma.js `scale` model, as an
218
+ * immutable class. Every configuration method returns a new scale, so a configured scale
219
+ * is safe to hold onto and share.
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * const scale = new ColorScale(['#2949e5', '#ffffff']);
224
+ * scale.at(0.5).toHex(); // the midpoint color
225
+ * scale.colors(5).map((color) => color.toHex()); // five evenly spaced colors
226
+ * scale.correctLightness().domain([0, 100]).at(72); // perceptually even, read in %
227
+ * ```
228
+ */
229
+ declare class ColorScale {
230
+ #private;
231
+ /**
232
+ * @param colors - the colors the scale runs through; a single color makes a flat scale
233
+ * @throws {TypeError} when `colors` is empty or `domain` has fewer than two values
234
+ */
235
+ constructor(colors: readonly ColorInput[], options?: ColorScaleOptions);
236
+ /** The color at `value`, read through the scale's domain, classes, gamma and padding. */
237
+ at(value: number): Color;
238
+ /** `count` colors spread evenly across the domain. */
239
+ colors(count: number): Color[];
240
+ mode(mode: InterpolationMode): ColorScale;
241
+ domain(domain: number[]): ColorScale;
242
+ gamma(gamma: number): ColorScale;
243
+ padding(padding: [number, number] | number): ColorScale;
244
+ correctLightness(correctLightness?: boolean): ColorScale;
245
+ classes(classes: number | number[]): ColorScale;
246
+ }
247
+ //#endregion
248
+ //#region src/getBestColorForText.d.ts
249
+ type BestColorForTextOptions = {
250
+ /** Always excluded from the result.*/
251
+ readonly backgroundColor: ColorInput;
252
+ /**
253
+ * Colors that are part of the palette but should not be considered as candidates.
254
+ * Useful in scenarios like the webinar pages color derivation logic, where we might include derived colors
255
+ * in the palette used by some fields, like `lowEmphasisTextColor`, but we want to exclude the derived colors
256
+ * from other fields, like `mainTextColor`.
257
+ */
258
+ readonly exclude?: readonly ColorInput[];
259
+ /**
260
+ * The colors to choose from. Black and white are included automatically, so an empty/absent palette
261
+ * still yields whichever of the two has best contrast against `backgroundColor`.
262
+ */
263
+ readonly palette?: readonly ColorInput[];
264
+ /** The type of text/shape that will use the resulting color. Defaults to `paragraphText` (4.5:1). */
265
+ readonly shape?: ColorShape;
266
+ };
267
+ /**
268
+ * Picks a color most suitable for a given text (or non-text) shape against the given background color.
269
+ * Accepts an optional color palette, but white and black are always included as options in addition to the palette.
270
+ * Also accepts a set of colors from the palette that should be excluded from consideration.
271
+ *
272
+ * @example
273
+ * ```ts
274
+ * getBestColorForText({ backgroundColor: '#2949e5' }).toHex(); // '#ffffff'
275
+ * ```
276
+ */
277
+ declare const getBestColorForText: ({ backgroundColor, exclude, palette, shape }: BestColorForTextOptions) => Color;
278
+ //#endregion
279
+ export { type AdjustColorForProperContrastOptions, type BestColorForTextOptions, Color, type ColorInput, ColorScale, type ColorScaleOptions, type ColorShape, type ColorTuple, type Hsl, type InterpolationMode, type Lab, type RgbTuple, type RgbaObject, type RgbaTuple, adjustColorForProperContrast, colorContrastRatiosByShape, getBestColorForText };
11
280
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/colorContrastRatiosByShape.ts"],"mappings":";;cAAa;EACX;EACA;EACA;EACA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/colorContrastRatiosByShape.ts","../src/Color.ts","../src/types.ts","../src/adjustColorForProperContrast.ts","../src/ColorScale.ts","../src/getBestColorForText.ts"],"mappings":";;cAAa;WACX;WACA;WACA;WACA;;;KAIU,0BAA0B;;;;;;;;;;;;;;cCyEzB;;;;;SAIJ,MAAM,OAAO,aAAa;SAK1B,QAAQ,KAAK,KAAK,iBAAY;SAK9B,QAAQ,KAAK,KAAK,iBAAY;;WAM5B;;WAGA;;WAGA;;WAGA;;;;;;EAOT,YAAY,OAAO;;EASnB;;EAKA;;EAKA;;EAKA;;EAKA,eAAe;EAIf,SAAS;EAIT,SAAS;;EAKT;;;;;EAQA;;;;;;;EAUA,SAAS,OAAO;;;;;;;;;;EAiBhB,QAAQ,gBAAgB;;EAKxB,OAAO,gBAAgB;;EAKvB,KAAK,eAAe,OAAM,oBAAiD;;EAK3E,MAAM,eAAe,OAAM,oBAAiD;;;;;EAQ5E,MACE,OAAO,YACP,gBACA,OAAM,oBACL;;EAwCH,UAAU,gBAAgB;;;;;;EAS1B,cAAc,oBAAoB;;;;;KC1QxB,YAAY,aAAa,eAAe;;KAGxC,aAAa,aAAa,eAAe,cAAc;;KAGvD,cAAc,aAAa,eAAe,cAAc;;KAGxD;WACD;WACA;WACA;WACA;;;;;;;;;;;;;KAcC,aAAa,QAAQ,aAAa;;KAGlC;WACD;WACA;WACA;;;;;;KAOC;WACD;WACA;WACA;;;;;;;;;;;KAYC;;;KC1BA;;WAED,iBAAiB;;WAGjB,iBAAiB;;WAGjB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6BN,iCACX,iBACA,iBACA,SACC,wCAAsC;;;KCzD7B;;;;;WAKD;;;;;WAMA;;;;;WAMA;;WAGA;;WAGA,OAAO;;WAGP;;;;;;;;;;;;;;;cAgDE;;;;;;EAmBX,YAAY,iBAAiB,cAAc,UAAS;;EAkEpD,GAAG,gBAAgB;;EAQnB,OAAO,gBAAgB;EAmBvB,KAAK,MAAM,oBAAoB;EAI/B,OAAO,mBAAmB;EAI1B,MAAM,gBAAgB;EAItB,QAAQ,qCAAqC;EAI7C,iBAAiB,6BAA0B;EAI3C,QAAQ,6BAA6B;;;;KCtN3B;;WAED,iBAAiB;;;;;;;WAQjB,mBAAmB;;;;;WAMnB,mBAAmB;;WAGnB,QAAQ;;;;;;;;;;;;cAaN,wBACX,iBACA,SACA,SACA,SACC,4BAA0B"}