@ankhorage/color-theory 0.0.8 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @ankhorage/color-theory
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 78cd2ca: Publish the canonical seven-harmony catalog, distinguish tetradic and square geometry, and report unreliable hue generation for achromatic or low-chroma primary colors.
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - be8317b: Add public contrast measurement, caller-defined foreground selection, and deterministic swatch-step selection with perceptual and contrast diagnostics.
14
+
3
15
  ## 0.0.8
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @ankhorage/color-theory
5
5
 
6
- ![license: MIT](././paradox/badges/license.svg) ![npm: v0.0.7](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
6
+ ![license: MIT](././paradox/badges/license.svg) ![npm: v0.0.8](././paradox/badges/npm.svg) ![runtime: bun](././paradox/badges/runtime.svg) ![typescript: strict](././paradox/badges/typescript.svg) ![eslint: checked](././paradox/badges/eslint.svg) ![prettier: checked](././paradox/badges/prettier.svg) ![build: checked](././paradox/badges/build.svg) ![tests: checked](././paradox/badges/tests.svg) ![docs: paradox](././paradox/badges/docs.svg)
7
7
 
8
8
  Standalone color theory, harmony, swatch, contrast, and theme color generation utilities.
9
9
 
@@ -16,62 +16,12 @@ Standalone color theory, harmony, swatch, contrast, and theme color generation u
16
16
  - [Module relationships](././paradox/diagrams/module-relationships.mmd)
17
17
  - [Export graph](././paradox/diagrams/export-graph.mmd)
18
18
  - [assertHexColor sequence](././paradox/diagrams/sequences/assert-hex-color.mmd)
19
+ - [createDefaultSemanticStatusSwatches sequence](././paradox/diagrams/sequences/create-default-semantic-status-swatches.mmd)
20
+ - [createSemanticStatusSwatches sequence](././paradox/diagrams/sequences/create-semantic-status-swatches.mmd)
21
+ - [generateColorSwatch sequence](././paradox/diagrams/sequences/generate-color-swatch.mmd)
22
+ - [getContrastRatio sequence](././paradox/diagrams/sequences/get-contrast-ratio.mmd)
19
23
  - [getReadableForeground sequence](././paradox/diagrams/sequences/get-readable-foreground.mmd)
20
24
  - [getThemeModePrimaryHex sequence](././paradox/diagrams/sequences/get-theme-mode-primary-hex.mmd)
21
25
  - [parseHexColorOrThrow sequence](././paradox/diagrams/sequences/parse-hex-color-or-throw.mmd)
22
-
23
- ## Architecture preview
24
-
25
- <details>
26
- <summary>Architecture overview</summary>
27
-
28
- ```mermaid
29
- graph TD
30
- package__ankhorage_color_theory["@ankhorage/color-theory"]
31
- entrypoint_src_index_ts["src/index.ts"]
32
- package__ankhorage_color_theory --> entrypoint_src_index_ts
33
- module_src_contrast_ts["src/contrast.ts"]
34
- package__ankhorage_color_theory -.-> module_src_contrast_ts
35
- module_src_contrast_ts --> module_src_hex_ts
36
- module_src_contrast_ts --> module_src_internal_culori_ts
37
- module_src_culori_fn_ts["src/culori-fn.ts"]
38
- package__ankhorage_color_theory -.-> module_src_culori_fn_ts
39
- module_src_culori_ts["src/culori.ts"]
40
- package__ankhorage_color_theory -.-> module_src_culori_ts
41
- module_src_harmony_ts["src/harmony.ts"]
42
- package__ankhorage_color_theory -.-> module_src_harmony_ts
43
- module_src_harmony_ts --> module_src_hex_ts
44
- module_src_harmony_ts --> module_src_internal_culori_ts
45
- module_src_hex_ts["src/hex.ts"]
46
- package__ankhorage_color_theory -.-> module_src_hex_ts
47
- module_src_index_ts["src/index.ts"]
48
- module_src_internal_culori_ts["src/internal-culori.ts"]
49
- package__ankhorage_color_theory -.-> module_src_internal_culori_ts
50
- module_src_internal_culori_ts --> module_src_hex_ts
51
- module_src_neutral_ts["src/neutral.ts"]
52
- package__ankhorage_color_theory -.-> module_src_neutral_ts
53
- module_src_neutral_ts --> module_src_harmony_ts
54
- module_src_neutral_ts --> module_src_hex_ts
55
- module_src_neutral_ts --> module_src_internal_culori_ts
56
- module_src_neutral_ts --> module_src_swatches_ts
57
- module_src_semantic_status_ts["src/semantic-status.ts"]
58
- package__ankhorage_color_theory -.-> module_src_semantic_status_ts
59
- module_src_semantic_status_ts --> module_src_hex_ts
60
- module_src_semantic_status_ts --> module_src_swatches_ts
61
- module_src_semantics_ts["src/semantics.ts"]
62
- package__ankhorage_color_theory -.-> module_src_semantics_ts
63
- module_src_semantics_ts --> module_src_harmony_ts
64
- module_src_semantics_ts --> module_src_swatches_ts
65
- module_src_swatches_ts["src/swatches.ts"]
66
- package__ankhorage_color_theory -.-> module_src_swatches_ts
67
- module_src_swatches_ts --> module_src_hex_ts
68
- module_src_swatches_ts --> module_src_internal_culori_ts
69
- module_src_theme_colors_ts["src/theme-colors.ts"]
70
- package__ankhorage_color_theory -.-> module_src_theme_colors_ts
71
- module_src_theme_colors_ts --> module_src_harmony_ts
72
- module_src_theme_colors_ts --> module_src_hex_ts
73
- module_src_theme_colors_ts --> module_src_neutral_ts
74
- module_src_theme_colors_ts --> module_src_swatches_ts
75
- ```
76
-
77
- </details>
26
+ - [selectColorSwatchStep sequence](././paradox/diagrams/sequences/select-color-swatch-step.mmd)
27
+ - [selectReadableForeground sequence](././paradox/diagrams/sequences/select-readable-foreground.mmd)
@@ -3,6 +3,31 @@ export interface ReadableForegroundResult {
3
3
  foreground: HexColor;
4
4
  contrast: number;
5
5
  }
6
+ export type ForegroundTiePolicy = 'first' | 'last';
7
+ export interface ForegroundSelectionCandidate {
8
+ foreground: HexColor;
9
+ contrast: number;
10
+ passesMinimum: boolean;
11
+ selected: boolean;
12
+ rejectionReason?: 'minimum-contrast' | 'weaker-contrast' | 'tie-break';
13
+ }
14
+ export interface ForegroundSelectionResult {
15
+ selected: ForegroundSelectionCandidate | null;
16
+ candidates: readonly ForegroundSelectionCandidate[];
17
+ minimumContrast: number;
18
+ tieBreak: {
19
+ policy: ForegroundTiePolicy;
20
+ applied: boolean;
21
+ };
22
+ }
23
+ /***
24
+ Measure the WCAG contrast ratio between two validated hex colors.
25
+ */
26
+ export declare function getContrastRatio(colorA: HexColor, colorB: HexColor): number;
27
+ /***
28
+ Select the strongest caller-provided foreground that meets a minimum WCAG contrast ratio.
29
+ */
30
+ export declare function selectReadableForeground(background: HexColor, foregrounds: readonly HexColor[], minimumContrast: number, tiePolicy: ForegroundTiePolicy): ForegroundSelectionResult;
6
31
  /***
7
32
  Return the readable black or white foreground color with the stronger contrast against a background color.
8
33
  */
@@ -1 +1 @@
1
- {"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,QAAQ,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD;;EAEE;AACF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,QAAQ,GAAG,wBAAwB,CASpF"}
1
+ {"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAItC,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,QAAQ,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,mBAAmB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,4BAA4B;IAC3C,UAAU,EAAE,QAAQ,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,kBAAkB,GAAG,iBAAiB,GAAG,WAAW,CAAC;CACxE;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,4BAA4B,GAAG,IAAI,CAAC;IAC9C,UAAU,EAAE,SAAS,4BAA4B,EAAE,CAAC;IACpD,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE;QACR,MAAM,EAAE,mBAAmB,CAAC;QAC5B,OAAO,EAAE,OAAO,CAAC;KAClB,CAAC;CACH;AAKD;;EAEE;AACF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAG,MAAM,CAE3E;AAED;;EAEE;AACF,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,QAAQ,EACpB,WAAW,EAAE,SAAS,QAAQ,EAAE,EAChC,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,mBAAmB,GAC7B,yBAAyB,CA8C3B;AAED;;EAEE;AACF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,QAAQ,GAAG,wBAAwB,CAUpF"}
package/dist/contrast.js CHANGED
@@ -2,15 +2,84 @@ import { parseHexColorOrThrow } from './hex';
2
2
  import { contrastRatio } from './internal-culori';
3
3
  const BLACK = parseHexColorOrThrow('#000000');
4
4
  const WHITE = parseHexColorOrThrow('#FFFFFF');
5
+ /***
6
+ Measure the WCAG contrast ratio between two validated hex colors.
7
+ */
8
+ export function getContrastRatio(colorA, colorB) {
9
+ return contrastRatio(colorA, colorB);
10
+ }
11
+ /***
12
+ Select the strongest caller-provided foreground that meets a minimum WCAG contrast ratio.
13
+ */
14
+ export function selectReadableForeground(background, foregrounds, minimumContrast, tiePolicy) {
15
+ assertMinimumContrast(minimumContrast);
16
+ if (foregrounds.length === 0) {
17
+ throw new Error('[color-theory] Foreground candidates must not be empty.');
18
+ }
19
+ const normalizedBackground = parseHexColorOrThrow(background);
20
+ const measured = foregrounds.map((foreground, index) => {
21
+ const normalizedForeground = parseHexColorOrThrow(foreground);
22
+ return {
23
+ foreground: normalizedForeground,
24
+ contrast: getContrastRatio(normalizedBackground, normalizedForeground),
25
+ index,
26
+ };
27
+ });
28
+ const passing = measured.filter(({ contrast }) => contrast >= minimumContrast);
29
+ const strongestContrast = Math.max(...passing.map(({ contrast }) => contrast));
30
+ const strongest = passing.filter(({ contrast }) => contrast === strongestContrast);
31
+ const selectedMeasured = tiePolicy === 'first' ? strongest.at(0) : strongest.at(-1);
32
+ const tieBreakApplied = strongest.length > 1;
33
+ const candidates = measured.map(({ foreground, contrast, index }) => {
34
+ const selected = selectedMeasured?.index === index;
35
+ return {
36
+ foreground,
37
+ contrast,
38
+ passesMinimum: contrast >= minimumContrast,
39
+ selected,
40
+ ...(!selected
41
+ ? {
42
+ rejectionReason: getForegroundRejectionReason(contrast, minimumContrast, strongestContrast),
43
+ }
44
+ : {}),
45
+ };
46
+ });
47
+ return {
48
+ selected: candidates.find(({ selected }) => selected) ?? null,
49
+ candidates,
50
+ minimumContrast,
51
+ tieBreak: { policy: tiePolicy, applied: tieBreakApplied },
52
+ };
53
+ }
5
54
  /***
6
55
  Return the readable black or white foreground color with the stronger contrast against a background color.
7
56
  */
8
57
  export function getReadableForeground(background) {
9
- const contrastOnBlack = contrastRatio(background, BLACK);
10
- const contrastOnWhite = contrastRatio(background, WHITE);
11
- if (contrastOnBlack >= contrastOnWhite) {
12
- return { foreground: BLACK, contrast: contrastOnBlack };
58
+ const result = selectReadableForeground(background, [BLACK, WHITE], 1, 'first');
59
+ if (!result.selected) {
60
+ throw new Error('[color-theory] Expected black or white to be a readable foreground.');
13
61
  }
14
- return { foreground: WHITE, contrast: contrastOnWhite };
62
+ return {
63
+ foreground: result.selected.foreground,
64
+ contrast: result.selected.contrast,
65
+ };
66
+ }
67
+ /***
68
+ Validate a WCAG contrast threshold.
69
+ */
70
+ function assertMinimumContrast(minimumContrast) {
71
+ if (!Number.isFinite(minimumContrast) || minimumContrast < 1 || minimumContrast > 21) {
72
+ throw new Error(`[color-theory] Minimum contrast must be a finite ratio from 1 through 21: ${minimumContrast}.`);
73
+ }
74
+ }
75
+ /***
76
+ Explain why a foreground candidate was not selected.
77
+ */
78
+ function getForegroundRejectionReason(contrast, minimumContrast, strongestContrast) {
79
+ if (contrast < minimumContrast)
80
+ return 'minimum-contrast';
81
+ if (contrast < strongestContrast)
82
+ return 'weaker-contrast';
83
+ return 'tie-break';
15
84
  }
16
85
  //# sourceMappingURL=contrast.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contrast.js","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOlD,MAAM,KAAK,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;AAC9C,MAAM,KAAK,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;AAE9C;;EAEE;AACF,MAAM,UAAU,qBAAqB,CAAC,UAAoB;IACxD,MAAM,eAAe,GAAG,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACzD,MAAM,eAAe,GAAG,aAAa,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAEzD,IAAI,eAAe,IAAI,eAAe,EAAE,CAAC;QACvC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC1D,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;AAC1D,CAAC","sourcesContent":["import type { HexColor } from './hex';\nimport { parseHexColorOrThrow } from './hex';\nimport { contrastRatio } from './internal-culori';\n\nexport interface ReadableForegroundResult {\n foreground: HexColor;\n contrast: number;\n}\n\nconst BLACK = parseHexColorOrThrow('#000000');\nconst WHITE = parseHexColorOrThrow('#FFFFFF');\n\n/***\n Return the readable black or white foreground color with the stronger contrast against a background color.\n*/\nexport function getReadableForeground(background: HexColor): ReadableForegroundResult {\n const contrastOnBlack = contrastRatio(background, BLACK);\n const contrastOnWhite = contrastRatio(background, WHITE);\n\n if (contrastOnBlack >= contrastOnWhite) {\n return { foreground: BLACK, contrast: contrastOnBlack };\n }\n\n return { foreground: WHITE, contrast: contrastOnWhite };\n}\n"]}
1
+ {"version":3,"file":"contrast.js","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AA2BlD,MAAM,KAAK,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;AAC9C,MAAM,KAAK,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;AAE9C;;EAEE;AACF,MAAM,UAAU,gBAAgB,CAAC,MAAgB,EAAE,MAAgB;IACjE,OAAO,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AACvC,CAAC;AAED;;EAEE;AACF,MAAM,UAAU,wBAAwB,CACtC,UAAoB,EACpB,WAAgC,EAChC,eAAuB,EACvB,SAA8B;IAE9B,qBAAqB,CAAC,eAAe,CAAC,CAAC;IACvC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,MAAM,oBAAoB,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAC9D,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,EAAE;QACrD,MAAM,oBAAoB,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;QAC9D,OAAO;YACL,UAAU,EAAE,oBAAoB;YAChC,QAAQ,EAAE,gBAAgB,CAAC,oBAAoB,EAAE,oBAAoB,CAAC;YACtE,KAAK;SACN,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,IAAI,eAAe,CAAC,CAAC;IAC/E,MAAM,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC/E,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,KAAK,iBAAiB,CAAC,CAAC;IACnF,MAAM,gBAAgB,GAAG,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,eAAe,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IAE7C,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,EAAE;QAClE,MAAM,QAAQ,GAAG,gBAAgB,EAAE,KAAK,KAAK,KAAK,CAAC;QACnD,OAAO;YACL,UAAU;YACV,QAAQ;YACR,aAAa,EAAE,QAAQ,IAAI,eAAe;YAC1C,QAAQ;YACR,GAAG,CAAC,CAAC,QAAQ;gBACX,CAAC,CAAC;oBACE,eAAe,EAAE,4BAA4B,CAC3C,QAAQ,EACR,eAAe,EACf,iBAAiB,CAClB;iBACF;gBACH,CAAC,CAAC,EAAE,CAAC;SAC+B,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,IAAI;QAC7D,UAAU;QACV,eAAe;QACf,QAAQ,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE;KAC1D,CAAC;AACJ,CAAC;AAED;;EAEE;AACF,MAAM,UAAU,qBAAqB,CAAC,UAAoB;IACxD,MAAM,MAAM,GAAG,wBAAwB,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IAChF,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACzF,CAAC;IAED,OAAO;QACL,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU;QACtC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;KACnC,CAAC;AACJ,CAAC;AAED;;EAEE;AACF,SAAS,qBAAqB,CAAC,eAAuB;IACpD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,eAAe,GAAG,CAAC,IAAI,eAAe,GAAG,EAAE,EAAE,CAAC;QACrF,MAAM,IAAI,KAAK,CACb,6EAA6E,eAAe,GAAG,CAChG,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;EAEE;AACF,SAAS,4BAA4B,CACnC,QAAgB,EAChB,eAAuB,EACvB,iBAAyB;IAEzB,IAAI,QAAQ,GAAG,eAAe;QAAE,OAAO,kBAAkB,CAAC;IAC1D,IAAI,QAAQ,GAAG,iBAAiB;QAAE,OAAO,iBAAiB,CAAC;IAC3D,OAAO,WAAW,CAAC;AACrB,CAAC","sourcesContent":["import type { HexColor } from './hex';\nimport { parseHexColorOrThrow } from './hex';\nimport { contrastRatio } from './internal-culori';\n\nexport interface ReadableForegroundResult {\n foreground: HexColor;\n contrast: number;\n}\n\nexport type ForegroundTiePolicy = 'first' | 'last';\n\nexport interface ForegroundSelectionCandidate {\n foreground: HexColor;\n contrast: number;\n passesMinimum: boolean;\n selected: boolean;\n rejectionReason?: 'minimum-contrast' | 'weaker-contrast' | 'tie-break';\n}\n\nexport interface ForegroundSelectionResult {\n selected: ForegroundSelectionCandidate | null;\n candidates: readonly ForegroundSelectionCandidate[];\n minimumContrast: number;\n tieBreak: {\n policy: ForegroundTiePolicy;\n applied: boolean;\n };\n}\n\nconst BLACK = parseHexColorOrThrow('#000000');\nconst WHITE = parseHexColorOrThrow('#FFFFFF');\n\n/***\n Measure the WCAG contrast ratio between two validated hex colors.\n*/\nexport function getContrastRatio(colorA: HexColor, colorB: HexColor): number {\n return contrastRatio(colorA, colorB);\n}\n\n/***\n Select the strongest caller-provided foreground that meets a minimum WCAG contrast ratio.\n*/\nexport function selectReadableForeground(\n background: HexColor,\n foregrounds: readonly HexColor[],\n minimumContrast: number,\n tiePolicy: ForegroundTiePolicy,\n): ForegroundSelectionResult {\n assertMinimumContrast(minimumContrast);\n if (foregrounds.length === 0) {\n throw new Error('[color-theory] Foreground candidates must not be empty.');\n }\n\n const normalizedBackground = parseHexColorOrThrow(background);\n const measured = foregrounds.map((foreground, index) => {\n const normalizedForeground = parseHexColorOrThrow(foreground);\n return {\n foreground: normalizedForeground,\n contrast: getContrastRatio(normalizedBackground, normalizedForeground),\n index,\n };\n });\n const passing = measured.filter(({ contrast }) => contrast >= minimumContrast);\n const strongestContrast = Math.max(...passing.map(({ contrast }) => contrast));\n const strongest = passing.filter(({ contrast }) => contrast === strongestContrast);\n const selectedMeasured = tiePolicy === 'first' ? strongest.at(0) : strongest.at(-1);\n const tieBreakApplied = strongest.length > 1;\n\n const candidates = measured.map(({ foreground, contrast, index }) => {\n const selected = selectedMeasured?.index === index;\n return {\n foreground,\n contrast,\n passesMinimum: contrast >= minimumContrast,\n selected,\n ...(!selected\n ? {\n rejectionReason: getForegroundRejectionReason(\n contrast,\n minimumContrast,\n strongestContrast,\n ),\n }\n : {}),\n } satisfies ForegroundSelectionCandidate;\n });\n\n return {\n selected: candidates.find(({ selected }) => selected) ?? null,\n candidates,\n minimumContrast,\n tieBreak: { policy: tiePolicy, applied: tieBreakApplied },\n };\n}\n\n/***\n Return the readable black or white foreground color with the stronger contrast against a background color.\n*/\nexport function getReadableForeground(background: HexColor): ReadableForegroundResult {\n const result = selectReadableForeground(background, [BLACK, WHITE], 1, 'first');\n if (!result.selected) {\n throw new Error('[color-theory] Expected black or white to be a readable foreground.');\n }\n\n return {\n foreground: result.selected.foreground,\n contrast: result.selected.contrast,\n };\n}\n\n/***\n Validate a WCAG contrast threshold.\n*/\nfunction assertMinimumContrast(minimumContrast: number): void {\n if (!Number.isFinite(minimumContrast) || minimumContrast < 1 || minimumContrast > 21) {\n throw new Error(\n `[color-theory] Minimum contrast must be a finite ratio from 1 through 21: ${minimumContrast}.`,\n );\n }\n}\n\n/***\n Explain why a foreground candidate was not selected.\n*/\nfunction getForegroundRejectionReason(\n contrast: number,\n minimumContrast: number,\n strongestContrast: number,\n): NonNullable<ForegroundSelectionCandidate['rejectionReason']> {\n if (contrast < minimumContrast) return 'minimum-contrast';\n if (contrast < strongestContrast) return 'weaker-contrast';\n return 'tie-break';\n}\n"]}
package/dist/harmony.d.ts CHANGED
@@ -1,7 +1,69 @@
1
1
  import type { HexColor } from './hex';
2
- export declare const COLOR_HARMONIES: readonly ["monochromatic", "analogous", "complementary", "triadic", "tetradic", "splitComplementary"];
3
- export type ColorHarmony = (typeof COLOR_HARMONIES)[number];
4
2
  export type GeneratedColorRole = 'primary' | 'secondary' | 'tertiary' | 'quaternary';
3
+ export declare const COLOR_HARMONY_CATALOG: readonly [{
4
+ readonly id: "monochromatic";
5
+ readonly label: "Monochromatic";
6
+ readonly description: "Uses one hue with ramp variation.";
7
+ readonly offsets: readonly [0];
8
+ readonly roles: readonly ["primary"];
9
+ readonly roleCount: 1;
10
+ }, {
11
+ readonly id: "analogous";
12
+ readonly label: "Analogous";
13
+ readonly description: "Uses the primary hue and its two neighboring hues at minus and plus 30 degrees.";
14
+ readonly offsets: readonly [0, -30, 30];
15
+ readonly roles: readonly ["primary", "secondary", "tertiary"];
16
+ readonly roleCount: 3;
17
+ }, {
18
+ readonly id: "complementary";
19
+ readonly label: "Complementary";
20
+ readonly description: "Pairs the primary hue with its opposite hue at 180 degrees.";
21
+ readonly offsets: readonly [0, 180];
22
+ readonly roles: readonly ["primary", "secondary"];
23
+ readonly roleCount: 2;
24
+ }, {
25
+ readonly id: "splitComplementary";
26
+ readonly label: "Split complementary";
27
+ readonly description: "Uses the primary hue with hues at 150 and 210 degrees.";
28
+ readonly offsets: readonly [0, 150, 210];
29
+ readonly roles: readonly ["primary", "secondary", "tertiary"];
30
+ readonly roleCount: 3;
31
+ }, {
32
+ readonly id: "triadic";
33
+ readonly label: "Triadic";
34
+ readonly description: "Uses three evenly spaced hues at 120-degree intervals.";
35
+ readonly offsets: readonly [0, 120, 240];
36
+ readonly roles: readonly ["primary", "secondary", "tertiary"];
37
+ readonly roleCount: 3;
38
+ }, {
39
+ readonly id: "tetradic";
40
+ readonly label: "Tetradic";
41
+ readonly description: "Uses a rectangular tetrad at 0, 60, 180, and 240 degrees.";
42
+ readonly offsets: readonly [0, 60, 180, 240];
43
+ readonly roles: readonly ["primary", "secondary", "tertiary", "quaternary"];
44
+ readonly roleCount: 4;
45
+ }, {
46
+ readonly id: "square";
47
+ readonly label: "Square";
48
+ readonly description: "Uses four evenly spaced hues at 90-degree intervals.";
49
+ readonly offsets: readonly [0, 90, 180, 270];
50
+ readonly roles: readonly ["primary", "secondary", "tertiary", "quaternary"];
51
+ readonly roleCount: 4;
52
+ }];
53
+ export type ColorHarmonyDefinition = (typeof COLOR_HARMONY_CATALOG)[number];
54
+ export type ColorHarmony = ColorHarmonyDefinition['id'];
55
+ export declare const COLOR_HARMONIES: readonly ColorHarmony[];
56
+ export declare const MIN_HUEFUL_CHROMA = 0.015;
57
+ export type HarmonyGenerationWarningCode = 'achromatic_primary' | 'low_chroma_primary';
58
+ export interface HarmonyGenerationWarning {
59
+ readonly code: HarmonyGenerationWarningCode;
60
+ readonly message: string;
61
+ }
62
+ export interface HarmonyGenerationDiagnostics {
63
+ readonly isHueReliable: boolean;
64
+ readonly primaryChroma: number;
65
+ readonly warnings: readonly HarmonyGenerationWarning[];
66
+ }
5
67
  export interface GeneratedHarmonyRoleColor {
6
68
  role: GeneratedColorRole;
7
69
  hex: HexColor;
@@ -15,6 +77,7 @@ export interface GeneratedHarmonyRoleColors {
15
77
  secondary?: GeneratedHarmonyRoleColor;
16
78
  tertiary?: GeneratedHarmonyRoleColor;
17
79
  quaternary?: GeneratedHarmonyRoleColor;
80
+ diagnostics: HarmonyGenerationDiagnostics;
18
81
  }
19
82
  /***
20
83
  Generate role-based harmony colors from a primary color and harmony strategy.
@@ -1 +1 @@
1
- {"version":3,"file":"harmony.d.ts","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,eAAO,MAAM,eAAe,uGAOlB,CAAC;AAEX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;AAErF,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,kBAAkB,CAAC;IACzB,GAAG,EAAE,QAAQ,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,WAAW,CAAC;CAClC;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAC7C,OAAO,EAAE,yBAAyB,CAAC;IACnC,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,UAAU,CAAC,EAAE,yBAAyB,CAAC;CACxC;AAoBD;;EAEE;AACF,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,QAAQ,EACtB,OAAO,EAAE,YAAY,GACpB,0BAA0B,CAsC5B"}
1
+ {"version":3,"file":"harmony.d.ts","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGtC,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,UAAU,GAAG,YAAY,CAAC;AAWrF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyDsB,CAAC;AAEzD,MAAM,MAAM,sBAAsB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5E,MAAM,MAAM,YAAY,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;AAExD,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAA8C,CAAC;AAElG,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAEvF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,CAAC;CACxD;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,kBAAkB,CAAC;IACzB,GAAG,EAAE,QAAQ,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,GAAG,WAAW,CAAC;CAClC;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,MAAM,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAC7C,OAAO,EAAE,yBAAyB,CAAC;IACnC,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,WAAW,EAAE,4BAA4B,CAAC;CAC3C;AAqCD;;EAEE;AACF,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,QAAQ,EACtB,OAAO,EAAE,YAAY,GACpB,0BAA0B,CAiC5B"}
package/dist/harmony.js CHANGED
@@ -1,49 +1,113 @@
1
1
  import { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';
2
- export const COLOR_HARMONIES = [
3
- 'monochromatic',
4
- 'analogous',
5
- 'complementary',
6
- 'triadic',
7
- 'tetradic',
8
- 'splitComplementary',
2
+ export const COLOR_HARMONY_CATALOG = [
3
+ {
4
+ id: 'monochromatic',
5
+ label: 'Monochromatic',
6
+ description: 'Uses one hue with ramp variation.',
7
+ offsets: [0],
8
+ roles: ['primary'],
9
+ roleCount: 1,
10
+ },
11
+ {
12
+ id: 'analogous',
13
+ label: 'Analogous',
14
+ description: 'Uses the primary hue and its two neighboring hues at minus and plus 30 degrees.',
15
+ offsets: [0, -30, 30],
16
+ roles: ['primary', 'secondary', 'tertiary'],
17
+ roleCount: 3,
18
+ },
19
+ {
20
+ id: 'complementary',
21
+ label: 'Complementary',
22
+ description: 'Pairs the primary hue with its opposite hue at 180 degrees.',
23
+ offsets: [0, 180],
24
+ roles: ['primary', 'secondary'],
25
+ roleCount: 2,
26
+ },
27
+ {
28
+ id: 'splitComplementary',
29
+ label: 'Split complementary',
30
+ description: 'Uses the primary hue with hues at 150 and 210 degrees.',
31
+ offsets: [0, 150, 210],
32
+ roles: ['primary', 'secondary', 'tertiary'],
33
+ roleCount: 3,
34
+ },
35
+ {
36
+ id: 'triadic',
37
+ label: 'Triadic',
38
+ description: 'Uses three evenly spaced hues at 120-degree intervals.',
39
+ offsets: [0, 120, 240],
40
+ roles: ['primary', 'secondary', 'tertiary'],
41
+ roleCount: 3,
42
+ },
43
+ {
44
+ id: 'tetradic',
45
+ label: 'Tetradic',
46
+ description: 'Uses a rectangular tetrad at 0, 60, 180, and 240 degrees.',
47
+ offsets: [0, 60, 180, 240],
48
+ roles: ['primary', 'secondary', 'tertiary', 'quaternary'],
49
+ roleCount: 4,
50
+ },
51
+ {
52
+ id: 'square',
53
+ label: 'Square',
54
+ description: 'Uses four evenly spaced hues at 90-degree intervals.',
55
+ offsets: [0, 90, 180, 270],
56
+ roles: ['primary', 'secondary', 'tertiary', 'quaternary'],
57
+ roleCount: 4,
58
+ },
9
59
  ];
10
- const ROLE_ORDER_BY_HARMONY = {
11
- monochromatic: ['primary'],
12
- complementary: ['primary', 'secondary'],
13
- analogous: ['primary', 'secondary', 'tertiary'],
14
- splitComplementary: ['primary', 'secondary', 'tertiary'],
15
- triadic: ['primary', 'secondary', 'tertiary'],
16
- tetradic: ['primary', 'secondary', 'tertiary', 'quaternary'],
17
- };
18
- const OFFSETS_BY_HARMONY = {
19
- monochromatic: [0],
20
- analogous: [0, -30, 30],
21
- complementary: [0, 180],
22
- splitComplementary: [0, 150, 210],
23
- triadic: [0, 120, 240],
24
- tetradic: [0, 90, 180, 270],
25
- };
60
+ export const COLOR_HARMONIES = COLOR_HARMONY_CATALOG.map(({ id }) => id);
61
+ export const MIN_HUEFUL_CHROMA = 0.015;
62
+ /***
63
+ Create diagnostics for the reliability of generated hue relationships.
64
+ */
65
+ function createHarmonyGenerationDiagnostics(primaryChroma) {
66
+ const warnings = [];
67
+ if (primaryChroma === 0) {
68
+ warnings.push({
69
+ code: 'achromatic_primary',
70
+ message: 'The selected primary is achromatic, so generated hue relationships are not visible.',
71
+ });
72
+ }
73
+ else if (primaryChroma < MIN_HUEFUL_CHROMA) {
74
+ warnings.push({
75
+ code: 'low_chroma_primary',
76
+ message: 'The selected primary has low chroma, so generated hue relationships may be weak.',
77
+ });
78
+ }
79
+ return {
80
+ isHueReliable: primaryChroma >= MIN_HUEFUL_CHROMA,
81
+ primaryChroma,
82
+ warnings,
83
+ };
84
+ }
85
+ /***
86
+ Resolve a complete harmony definition from the canonical public catalog.
87
+ */
88
+ function getHarmonyDefinition(harmony) {
89
+ const definition = COLOR_HARMONY_CATALOG.find(({ id }) => id === harmony);
90
+ if (!definition) {
91
+ throw new Error(`[color-theory] Missing definition for harmony ${harmony}.`);
92
+ }
93
+ return definition;
94
+ }
26
95
  /***
27
96
  Generate role-based harmony colors from a primary color and harmony strategy.
28
97
  */
29
98
  export function generateHarmonyRoleColors(primaryColor, harmony) {
30
99
  const base = parseHexToOklch(primaryColor);
31
- const roles = ROLE_ORDER_BY_HARMONY[harmony];
32
- const offsets = OFFSETS_BY_HARMONY[harmony];
100
+ const definition = getHarmonyDefinition(harmony);
33
101
  const baseHue = normalizeHueDegrees(base.h);
34
- const colors = [];
35
- for (let index = 0; index < roles.length; index++) {
36
- const role = roles[index];
37
- if (!role)
38
- continue;
39
- const hueDegrees = normalizeHueDegrees(baseHue + (offsets[index] ?? 0));
40
- colors.push({
102
+ const colors = definition.roles.map((role, index) => {
103
+ const hueDegrees = normalizeHueDegrees(baseHue + (definition.offsets.at(index) ?? 0));
104
+ return {
41
105
  role,
42
106
  hex: role === 'primary' ? primaryColor : oklchToHex({ ...base, h: hueDegrees }),
43
107
  hueDegrees,
44
108
  source: role === 'primary' ? 'selected' : 'generated',
45
- });
46
- }
109
+ };
110
+ });
47
111
  const primary = colors.find((color) => color.role === 'primary');
48
112
  if (!primary) {
49
113
  throw new Error('[color-theory] Expected generated harmony role colors to include primary.');
@@ -58,6 +122,7 @@ export function generateHarmonyRoleColors(primaryColor, harmony) {
58
122
  ...(secondary ? { secondary } : {}),
59
123
  ...(tertiary ? { tertiary } : {}),
60
124
  ...(quaternary ? { quaternary } : {}),
125
+ diagnostics: createHarmonyGenerationDiagnostics(base.c),
61
126
  };
62
127
  }
63
128
  //# sourceMappingURL=harmony.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"harmony.js","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HexColor } from './hex';\nimport { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';\n\nexport const COLOR_HARMONIES = [\n 'monochromatic',\n 'analogous',\n 'complementary',\n 'triadic',\n 'tetradic',\n 'splitComplementary',\n] as const;\n\nexport type ColorHarmony = (typeof COLOR_HARMONIES)[number];\n\nexport type GeneratedColorRole = 'primary' | 'secondary' | 'tertiary' | 'quaternary';\n\nexport interface GeneratedHarmonyRoleColor {\n role: GeneratedColorRole;\n hex: HexColor;\n hueDegrees: number;\n source: 'selected' | 'generated';\n}\n\nexport interface GeneratedHarmonyRoleColors {\n harmony: ColorHarmony;\n colors: readonly GeneratedHarmonyRoleColor[];\n primary: GeneratedHarmonyRoleColor;\n secondary?: GeneratedHarmonyRoleColor;\n tertiary?: GeneratedHarmonyRoleColor;\n quaternary?: GeneratedHarmonyRoleColor;\n}\n\nconst ROLE_ORDER_BY_HARMONY: Record<ColorHarmony, readonly GeneratedColorRole[]> = {\n monochromatic: ['primary'],\n complementary: ['primary', 'secondary'],\n analogous: ['primary', 'secondary', 'tertiary'],\n splitComplementary: ['primary', 'secondary', 'tertiary'],\n triadic: ['primary', 'secondary', 'tertiary'],\n tetradic: ['primary', 'secondary', 'tertiary', 'quaternary'],\n};\n\nconst OFFSETS_BY_HARMONY: Record<ColorHarmony, readonly number[]> = {\n monochromatic: [0],\n analogous: [0, -30, 30],\n complementary: [0, 180],\n splitComplementary: [0, 150, 210],\n triadic: [0, 120, 240],\n tetradic: [0, 90, 180, 270],\n};\n\n/***\n Generate role-based harmony colors from a primary color and harmony strategy.\n*/\nexport function generateHarmonyRoleColors(\n primaryColor: HexColor,\n harmony: ColorHarmony,\n): GeneratedHarmonyRoleColors {\n const base = parseHexToOklch(primaryColor);\n const roles = ROLE_ORDER_BY_HARMONY[harmony];\n const offsets = OFFSETS_BY_HARMONY[harmony];\n const baseHue = normalizeHueDegrees(base.h);\n\n const colors: GeneratedHarmonyRoleColor[] = [];\n\n for (let index = 0; index < roles.length; index++) {\n const role = roles[index];\n if (!role) continue;\n\n const hueDegrees = normalizeHueDegrees(baseHue + (offsets[index] ?? 0));\n colors.push({\n role,\n hex: role === 'primary' ? primaryColor : oklchToHex({ ...base, h: hueDegrees }),\n hueDegrees,\n source: role === 'primary' ? 'selected' : 'generated',\n });\n }\n\n const primary = colors.find((color) => color.role === 'primary');\n if (!primary) {\n throw new Error('[color-theory] Expected generated harmony role colors to include primary.');\n }\n\n const secondary = colors.find((color) => color.role === 'secondary');\n const tertiary = colors.find((color) => color.role === 'tertiary');\n const quaternary = colors.find((color) => color.role === 'quaternary');\n\n return {\n harmony,\n colors,\n primary,\n ...(secondary ? { secondary } : {}),\n ...(tertiary ? { tertiary } : {}),\n ...(quaternary ? { quaternary } : {}),\n };\n}\n"]}
1
+ {"version":3,"file":"harmony.js","sourceRoot":"","sources":["../src/harmony.ts"],"names":[],"mappings":"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","sourcesContent":["import type { HexColor } from './hex';\nimport { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';\n\nexport type GeneratedColorRole = 'primary' | 'secondary' | 'tertiary' | 'quaternary';\n\ninterface ColorHarmonyCatalogEntry {\n readonly id: string;\n readonly label: string;\n readonly description: string;\n readonly offsets: readonly number[];\n readonly roles: readonly GeneratedColorRole[];\n readonly roleCount: 1 | 2 | 3 | 4;\n}\n\nexport const COLOR_HARMONY_CATALOG = [\n {\n id: 'monochromatic',\n label: 'Monochromatic',\n description: 'Uses one hue with ramp variation.',\n offsets: [0],\n roles: ['primary'],\n roleCount: 1,\n },\n {\n id: 'analogous',\n label: 'Analogous',\n description: 'Uses the primary hue and its two neighboring hues at minus and plus 30 degrees.',\n offsets: [0, -30, 30],\n roles: ['primary', 'secondary', 'tertiary'],\n roleCount: 3,\n },\n {\n id: 'complementary',\n label: 'Complementary',\n description: 'Pairs the primary hue with its opposite hue at 180 degrees.',\n offsets: [0, 180],\n roles: ['primary', 'secondary'],\n roleCount: 2,\n },\n {\n id: 'splitComplementary',\n label: 'Split complementary',\n description: 'Uses the primary hue with hues at 150 and 210 degrees.',\n offsets: [0, 150, 210],\n roles: ['primary', 'secondary', 'tertiary'],\n roleCount: 3,\n },\n {\n id: 'triadic',\n label: 'Triadic',\n description: 'Uses three evenly spaced hues at 120-degree intervals.',\n offsets: [0, 120, 240],\n roles: ['primary', 'secondary', 'tertiary'],\n roleCount: 3,\n },\n {\n id: 'tetradic',\n label: 'Tetradic',\n description: 'Uses a rectangular tetrad at 0, 60, 180, and 240 degrees.',\n offsets: [0, 60, 180, 240],\n roles: ['primary', 'secondary', 'tertiary', 'quaternary'],\n roleCount: 4,\n },\n {\n id: 'square',\n label: 'Square',\n description: 'Uses four evenly spaced hues at 90-degree intervals.',\n offsets: [0, 90, 180, 270],\n roles: ['primary', 'secondary', 'tertiary', 'quaternary'],\n roleCount: 4,\n },\n] as const satisfies readonly ColorHarmonyCatalogEntry[];\n\nexport type ColorHarmonyDefinition = (typeof COLOR_HARMONY_CATALOG)[number];\nexport type ColorHarmony = ColorHarmonyDefinition['id'];\n\nexport const COLOR_HARMONIES: readonly ColorHarmony[] = COLOR_HARMONY_CATALOG.map(({ id }) => id);\n\nexport const MIN_HUEFUL_CHROMA = 0.015;\n\nexport type HarmonyGenerationWarningCode = 'achromatic_primary' | 'low_chroma_primary';\n\nexport interface HarmonyGenerationWarning {\n readonly code: HarmonyGenerationWarningCode;\n readonly message: string;\n}\n\nexport interface HarmonyGenerationDiagnostics {\n readonly isHueReliable: boolean;\n readonly primaryChroma: number;\n readonly warnings: readonly HarmonyGenerationWarning[];\n}\n\nexport interface GeneratedHarmonyRoleColor {\n role: GeneratedColorRole;\n hex: HexColor;\n hueDegrees: number;\n source: 'selected' | 'generated';\n}\n\nexport interface GeneratedHarmonyRoleColors {\n harmony: ColorHarmony;\n colors: readonly GeneratedHarmonyRoleColor[];\n primary: GeneratedHarmonyRoleColor;\n secondary?: GeneratedHarmonyRoleColor;\n tertiary?: GeneratedHarmonyRoleColor;\n quaternary?: GeneratedHarmonyRoleColor;\n diagnostics: HarmonyGenerationDiagnostics;\n}\n\n/***\n Create diagnostics for the reliability of generated hue relationships.\n*/\nfunction createHarmonyGenerationDiagnostics(primaryChroma: number): HarmonyGenerationDiagnostics {\n const warnings: HarmonyGenerationWarning[] = [];\n if (primaryChroma === 0) {\n warnings.push({\n code: 'achromatic_primary',\n message:\n 'The selected primary is achromatic, so generated hue relationships are not visible.',\n });\n } else if (primaryChroma < MIN_HUEFUL_CHROMA) {\n warnings.push({\n code: 'low_chroma_primary',\n message: 'The selected primary has low chroma, so generated hue relationships may be weak.',\n });\n }\n return {\n isHueReliable: primaryChroma >= MIN_HUEFUL_CHROMA,\n primaryChroma,\n warnings,\n };\n}\n\n/***\n Resolve a complete harmony definition from the canonical public catalog.\n*/\nfunction getHarmonyDefinition(harmony: ColorHarmony): ColorHarmonyDefinition {\n const definition = COLOR_HARMONY_CATALOG.find(({ id }) => id === harmony);\n if (!definition) {\n throw new Error(`[color-theory] Missing definition for harmony ${harmony}.`);\n }\n return definition;\n}\n\n/***\n Generate role-based harmony colors from a primary color and harmony strategy.\n*/\nexport function generateHarmonyRoleColors(\n primaryColor: HexColor,\n harmony: ColorHarmony,\n): GeneratedHarmonyRoleColors {\n const base = parseHexToOklch(primaryColor);\n const definition = getHarmonyDefinition(harmony);\n const baseHue = normalizeHueDegrees(base.h);\n\n const colors = definition.roles.map((role, index): GeneratedHarmonyRoleColor => {\n const hueDegrees = normalizeHueDegrees(baseHue + (definition.offsets.at(index) ?? 0));\n return {\n role,\n hex: role === 'primary' ? primaryColor : oklchToHex({ ...base, h: hueDegrees }),\n hueDegrees,\n source: role === 'primary' ? 'selected' : 'generated',\n };\n });\n\n const primary = colors.find((color) => color.role === 'primary');\n if (!primary) {\n throw new Error('[color-theory] Expected generated harmony role colors to include primary.');\n }\n\n const secondary = colors.find((color) => color.role === 'secondary');\n const tertiary = colors.find((color) => color.role === 'tertiary');\n const quaternary = colors.find((color) => color.role === 'quaternary');\n\n return {\n harmony,\n colors,\n primary,\n ...(secondary ? { secondary } : {}),\n ...(tertiary ? { tertiary } : {}),\n ...(quaternary ? { quaternary } : {}),\n diagnostics: createHarmonyGenerationDiagnostics(base.c),\n };\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './contrast';
2
2
  export * from './harmony';
3
3
  export * from './hex';
4
4
  export * from './neutral';
5
+ export * from './selection';
5
6
  export * from './semantic-status';
6
7
  export * from './semantics';
7
8
  export * from './swatches';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from './contrast';
2
2
  export * from './harmony';
3
3
  export * from './hex';
4
4
  export * from './neutral';
5
+ export * from './selection';
5
6
  export * from './semantic-status';
6
7
  export * from './semantics';
7
8
  export * from './swatches';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC","sourcesContent":["export * from './contrast';\nexport * from './harmony';\nexport * from './hex';\nexport * from './neutral';\nexport * from './semantic-status';\nexport * from './semantics';\nexport * from './swatches';\nexport * from './theme-colors';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAClC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC","sourcesContent":["export * from './contrast';\nexport * from './harmony';\nexport * from './hex';\nexport * from './neutral';\nexport * from './selection';\nexport * from './semantic-status';\nexport * from './semantics';\nexport * from './swatches';\nexport * from './theme-colors';\n"]}
package/dist/neutral.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import type { GeneratedHarmonyRoleColors } from './harmony';
1
+ import { type GeneratedHarmonyRoleColors } from './harmony';
2
2
  import type { HexColor } from './hex';
3
3
  import { type ColorSwatch, type ColorSwatchDiagnostics } from './swatches';
4
- export declare const MIN_HUEFUL_CHROMA = 0.015;
5
4
  export interface NeutralSwatchResult {
6
5
  neutralKeyColor: HexColor;
7
6
  neutral: ColorSwatch;
@@ -1 +1 @@
1
- {"version":3,"file":"neutral.d.ts","sourceRoot":"","sources":["../src/neutral.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,0BAA0B,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAE5B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,QAAQ,CAAC;IAC1B,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAED,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,QAAQ,CAAC;IAC1B,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAkCD;;EAEE;AACF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,0BAA0B,GAAG,mBAAmB,CAuBjG"}
1
+ {"version":3,"file":"neutral.d.ts","sourceRoot":"","sources":["../src/neutral.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,0BAA0B,EAAqB,MAAM,WAAW,CAAC;AAClG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEtC,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAE5B,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,QAAQ,CAAC;IAC1B,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAED,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,QAAQ,CAAC;IAC1B,WAAW,EAAE,sBAAsB,CAAC;CACrC;AAwBD;;EAEE;AACF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,0BAA0B,GAAG,mBAAmB,CAuBjG"}
package/dist/neutral.js CHANGED
@@ -1,22 +1,16 @@
1
+ import { MIN_HUEFUL_CHROMA } from './harmony';
1
2
  import { normalizeHueDegrees, oklchToHex, parseHexToOklch } from './internal-culori';
2
3
  import { COLOR_SWATCH_BASE_LIGHTNESS, generateColorSwatch, } from './swatches';
3
- export const MIN_HUEFUL_CHROMA = 0.015;
4
4
  /***
5
5
  Pick the harmony color that should tint the generated neutral swatch.
6
6
  */
7
7
  function pickTintSourceHex(roleColors, harmony) {
8
- const byMapping = {
9
- monochromatic: roleColors.primary.hex,
10
- complementary: roleColors.secondary?.hex,
11
- analogous: roleColors.tertiary?.hex,
12
- splitComplementary: roleColors.tertiary?.hex,
13
- triadic: roleColors.tertiary?.hex,
14
- tetradic: roleColors.tertiary?.hex,
15
- };
16
- return (byMapping[harmony] ??
17
- roleColors.tertiary?.hex ??
18
- roleColors.secondary?.hex ??
19
- roleColors.primary.hex);
8
+ if (harmony === 'monochromatic')
9
+ return roleColors.primary.hex;
10
+ if (harmony === 'complementary') {
11
+ return roleColors.secondary?.hex ?? roleColors.primary.hex;
12
+ }
13
+ return roleColors.tertiary?.hex ?? roleColors.secondary?.hex ?? roleColors.primary.hex;
20
14
  }
21
15
  /***
22
16
  Clamp a number to a finite minimum and maximum range.