@loidolt/theme-tokens 0.4.0 → 0.6.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
@@ -14,3 +14,15 @@ import { colors, semantic, spacing, tokens } from '@loidolt/theme-tokens';
14
14
  @import '@loidolt/theme-tokens/css';
15
15
  @import '@loidolt/theme-tokens/css/dark';
16
16
  ```
17
+
18
+ WCAG contrast arithmetic, for colours chosen at runtime:
19
+
20
+ ```ts
21
+ import { getContrastRatio, getContrastTextColor, meetsContrast } from '@loidolt/theme-tokens';
22
+
23
+ meetsContrast('#767676', '#ffffff'); // true — AA for body text
24
+ getContrastTextColor(userColour); // the theme ink that reads best on it
25
+ ```
26
+
27
+ Named media frames live in `aspectRatio` (`square`, `video`, `photo`, `portrait`, `wide`) and as
28
+ `--loidolt-aspect-*`.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,53 @@
1
+ /**
2
+ * WCAG 2.x contrast arithmetic, shared by the token tests and by applications that need to pick
3
+ * or validate a colour at runtime (a user-chosen swatch, a generated label). Pure functions over
4
+ * hex strings, so they run anywhere — build scripts, servers, and the browser.
5
+ *
6
+ * @see https://www.w3.org/TR/WCAG22/#dfn-relative-luminance
7
+ */
8
+ type ContrastLevel = 'AA' | 'AAA';
9
+ type TextSize = 'normal' | 'large';
10
+ interface ContrastValidation {
11
+ ratio: number;
12
+ /** `ratio` to two decimals, e.g. `'4.52:1'`. */
13
+ ratioString: string;
14
+ passesAANormal: boolean;
15
+ passesAALarge: boolean;
16
+ passesAAANormal: boolean;
17
+ passesAAALarge: boolean;
18
+ }
19
+ /** Minimum ratio per level and text size. Large text is 18.66px bold or 24px regular and up. */
20
+ declare const contrastRequirements: {
21
+ readonly AA: {
22
+ readonly normal: 4.5;
23
+ readonly large: 3;
24
+ };
25
+ readonly AAA: {
26
+ readonly normal: 7;
27
+ readonly large: 4.5;
28
+ };
29
+ };
30
+ /** Relative luminance, from 0 (black) to 1 (white). Accepts `#rgb` and `#rrggbb`. */
31
+ declare function getLuminance(hex: string): number;
32
+ /** Contrast ratio between two colours, from 1 to 21. Order does not matter. */
33
+ declare function getContrastRatio(foreground: string, background: string): number;
34
+ /** Whether a pair clears the WCAG threshold for the given level and text size. */
35
+ declare function meetsContrast(foreground: string, background: string, { level, size }?: {
36
+ level?: ContrastLevel;
37
+ size?: TextSize;
38
+ }): boolean;
39
+ /** Every threshold at once, for reporting. */
40
+ declare function validateContrast(foreground: string, background: string): ContrastValidation;
41
+ /**
42
+ * Whichever of two inks reads better on `background` — by measured ratio, not a luminance
43
+ * cut-off, so it stays right for any pair you pass. Defaults to the theme's own deep ink and
44
+ * panel, rather than pure black and white.
45
+ */
46
+ declare function getContrastTextColor(background: string, { dark, light }?: {
47
+ dark?: string;
48
+ light?: string;
49
+ }): string;
50
+
1
51
  /**
2
52
  * Design tokens for Loidolt applications.
3
53
  *
@@ -120,6 +170,17 @@ declare const breakpoints: {
120
170
  readonly expanded: "1024px";
121
171
  readonly wide: "1400px";
122
172
  };
173
+ /**
174
+ * Named frames for media, as CSS `aspect-ratio` values. `Thumbnail`, `AspectRatio` and the media
175
+ * components accept these names so a gallery and its lightbox agree on shape.
176
+ */
177
+ declare const aspectRatio: {
178
+ readonly square: "1 / 1";
179
+ readonly video: "16 / 9";
180
+ readonly photo: "4 / 3";
181
+ readonly portrait: "3 / 4";
182
+ readonly wide: "21 / 9";
183
+ };
123
184
  /**
124
185
  * The role layer. Every value links to a primitive, so overriding a primitive propagates and
125
186
  * overriding a role restyles every component that plays it.
@@ -322,6 +383,13 @@ declare const tokens: {
322
383
  readonly expanded: "1024px";
323
384
  readonly wide: "1400px";
324
385
  };
386
+ readonly aspectRatio: {
387
+ readonly square: "1 / 1";
388
+ readonly video: "16 / 9";
389
+ readonly photo: "4 / 3";
390
+ readonly portrait: "3 / 4";
391
+ readonly wide: "21 / 9";
392
+ };
325
393
  readonly semantic: {
326
394
  readonly background: string;
327
395
  readonly surface: string;
@@ -399,4 +467,4 @@ declare const tokens: {
399
467
  };
400
468
  type ThemeTokens = typeof tokens;
401
469
 
402
- export { type ThemeTokens, type TokenRef, borders, breakpoints, colors, cssVarName, darkSemantic, tokens as default, flattenTokens, generateCss, generateDarkCss, motion, semantic, shadows, sizing, spacing, tokens, typography, zIndex };
470
+ export { type ContrastLevel, type ContrastValidation, type TextSize, type ThemeTokens, type TokenRef, aspectRatio, borders, breakpoints, colors, contrastRequirements, cssVarName, darkSemantic, tokens as default, flattenTokens, generateCss, generateDarkCss, getContrastRatio, getContrastTextColor, getLuminance, meetsContrast, motion, semantic, shadows, sizing, spacing, tokens, typography, validateContrast, zIndex };
package/dist/index.js CHANGED
@@ -1,3 +1,46 @@
1
+ // src/contrast.ts
2
+ var contrastRequirements = {
3
+ AA: { normal: 4.5, large: 3 },
4
+ AAA: { normal: 7, large: 4.5 }
5
+ };
6
+ var channels = (hex, caller) => {
7
+ const match = /^#?([\da-f]{3}|[\da-f]{6})$/i.exec(hex.trim());
8
+ if (!match) throw new RangeError(`${caller}: invalid hex colour ${JSON.stringify(hex)}`);
9
+ const digits = match[1].length === 3 ? [...match[1]].map((digit) => digit + digit).join("") : match[1];
10
+ return [0, 2, 4].map((index) => parseInt(digits.slice(index, index + 2), 16));
11
+ };
12
+ var luminanceOf = (hex, caller) => {
13
+ const [r, g, b] = channels(hex, caller).map((channel) => channel / 255).map((channel) => channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
14
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
15
+ };
16
+ function getLuminance(hex) {
17
+ return luminanceOf(hex, "getLuminance");
18
+ }
19
+ function getContrastRatio(foreground, background) {
20
+ const [high, low] = [
21
+ luminanceOf(foreground, "getContrastRatio"),
22
+ luminanceOf(background, "getContrastRatio")
23
+ ].sort((a, b) => b - a);
24
+ return (high + 0.05) / (low + 0.05);
25
+ }
26
+ function meetsContrast(foreground, background, { level = "AA", size = "normal" } = {}) {
27
+ return getContrastRatio(foreground, background) >= contrastRequirements[level][size];
28
+ }
29
+ function validateContrast(foreground, background) {
30
+ const ratio = getContrastRatio(foreground, background);
31
+ return {
32
+ ratio,
33
+ ratioString: `${ratio.toFixed(2)}:1`,
34
+ passesAANormal: ratio >= contrastRequirements.AA.normal,
35
+ passesAALarge: ratio >= contrastRequirements.AA.large,
36
+ passesAAANormal: ratio >= contrastRequirements.AAA.normal,
37
+ passesAAALarge: ratio >= contrastRequirements.AAA.large
38
+ };
39
+ }
40
+ function getContrastTextColor(background, { dark = "#20231d", light = "#f5f2e9" } = {}) {
41
+ return getContrastRatio(dark, background) >= getContrastRatio(light, background) ? dark : light;
42
+ }
43
+
1
44
  // src/index.ts
2
45
  var ref = (name) => ({ $ref: `--loidolt-${name}` });
3
46
  var isRef = (value) => typeof value === "object" && value !== null && "$ref" in value;
@@ -88,6 +131,13 @@ var zIndex = {
88
131
  skipLink: "1000"
89
132
  };
90
133
  var breakpoints = { compact: "760px", expanded: "1024px", wide: "1400px" };
134
+ var aspectRatio = {
135
+ square: "1 / 1",
136
+ video: "16 / 9",
137
+ photo: "4 / 3",
138
+ portrait: "3 / 4",
139
+ wide: "21 / 9"
140
+ };
91
141
  var semanticDefs = {
92
142
  background: ref("color-paper"),
93
143
  surface: ref("color-panel"),
@@ -141,7 +191,8 @@ var PRIMITIVE_GROUPS = [
141
191
  ["border", borders],
142
192
  ["shadow", shadows],
143
193
  ["motion", motion],
144
- ["z", zIndex]
194
+ ["z", zIndex],
195
+ ["aspect", aspectRatio]
145
196
  ];
146
197
  var flattenInto = (prefix, tree, out) => {
147
198
  for (const [key, value] of Object.entries(tree)) {
@@ -245,20 +296,27 @@ var tokens = {
245
296
  motion,
246
297
  zIndex,
247
298
  breakpoints,
299
+ aspectRatio,
248
300
  semantic,
249
301
  darkSemantic
250
302
  };
251
303
  var index_default = tokens;
252
304
  export {
305
+ aspectRatio,
253
306
  borders,
254
307
  breakpoints,
255
308
  colors,
309
+ contrastRequirements,
256
310
  cssVarName,
257
311
  darkSemantic,
258
312
  index_default as default,
259
313
  flattenTokens,
260
314
  generateCss,
261
315
  generateDarkCss,
316
+ getContrastRatio,
317
+ getContrastTextColor,
318
+ getLuminance,
319
+ meetsContrast,
262
320
  motion,
263
321
  semantic,
264
322
  shadows,
@@ -266,5 +324,6 @@ export {
266
324
  spacing,
267
325
  tokens,
268
326
  typography,
327
+ validateContrast,
269
328
  zIndex
270
329
  };
package/dist/tokens.css CHANGED
@@ -71,6 +71,11 @@
71
71
  --loidolt-z-toast: 80;
72
72
  --loidolt-z-tooltip: 90;
73
73
  --loidolt-z-skip-link: 1000;
74
+ --loidolt-aspect-square: 1 / 1;
75
+ --loidolt-aspect-video: 16 / 9;
76
+ --loidolt-aspect-photo: 4 / 3;
77
+ --loidolt-aspect-portrait: 3 / 4;
78
+ --loidolt-aspect-wide: 21 / 9;
74
79
  --loidolt-background: var(--loidolt-color-paper);
75
80
  --loidolt-surface: var(--loidolt-color-panel);
76
81
  --loidolt-surface-alt: var(--loidolt-color-panel-alt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loidolt/theme-tokens",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Typed design tokens for Loidolt applications",
5
5
  "license": "MIT",
6
6
  "type": "module",