@ankhorage/zora 0.16.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +74 -0
- package/README.md +27 -22
- package/dist/components/heading/resolveHeadingRecipe.d.ts +2 -2
- package/dist/components/heading/resolveHeadingRecipe.d.ts.map +1 -1
- package/dist/components/heading/resolveHeadingRecipe.js.map +1 -1
- package/dist/components/text/resolveTextRecipe.d.ts +2 -2
- package/dist/components/text/resolveTextRecipe.d.ts.map +1 -1
- package/dist/components/text/resolveTextRecipe.js.map +1 -1
- package/dist/patterns/theme-composer/ThemeComposer.d.ts.map +1 -1
- package/dist/patterns/theme-composer/ThemeComposer.js +101 -86
- package/dist/patterns/theme-composer/ThemeComposer.js.map +1 -1
- package/dist/patterns/theme-composer/index.d.ts +1 -1
- package/dist/patterns/theme-composer/index.d.ts.map +1 -1
- package/dist/patterns/theme-composer/index.js.map +1 -1
- package/dist/patterns/theme-composer/types.d.ts +3 -13
- package/dist/patterns/theme-composer/types.d.ts.map +1 -1
- package/dist/patterns/theme-composer/types.js.map +1 -1
- package/dist/theme/createZoraThemeConfig.d.ts +1 -1
- package/dist/theme/createZoraThemeConfig.d.ts.map +1 -1
- package/dist/theme/createZoraThemeConfig.js +5 -6
- package/dist/theme/createZoraThemeConfig.js.map +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/types.d.ts +16 -11
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/types.js +1 -20
- package/dist/theme/types.js.map +1 -1
- package/dist/theme/useZoraTheme.d.ts +1 -1
- package/dist/theme/zoraDefaultTheme.js +1 -1
- package/dist/theme/zoraDefaultTheme.js.map +1 -1
- package/package.json +4 -4
- package/src/components/heading/resolveHeadingRecipe.test.ts +30 -5
- package/src/components/heading/resolveHeadingRecipe.ts +6 -6
- package/src/components/text/resolveTextRecipe.test.ts +30 -5
- package/src/components/text/resolveTextRecipe.ts +6 -6
- package/src/patterns/theme-composer/ThemeComposer.test.ts +128 -114
- package/src/patterns/theme-composer/ThemeComposer.tsx +130 -128
- package/src/patterns/theme-composer/index.ts +1 -6
- package/src/patterns/theme-composer/types.ts +4 -15
- package/src/showcaseCoverage.test.ts +14 -0
- package/src/theme/createZoraThemeConfig.test.ts +51 -26
- package/src/theme/createZoraThemeConfig.ts +7 -7
- package/src/theme/index.ts +1 -3
- package/src/theme/types.ts +22 -34
- package/src/theme/zoraDefaultTheme.ts +1 -1
- package/dist/internal/color/colorToneRecipes.d.ts +0 -23
- package/dist/internal/color/colorToneRecipes.d.ts.map +0 -1
- package/dist/internal/color/colorToneRecipes.js +0 -139
- package/dist/internal/color/colorToneRecipes.js.map +0 -1
- package/dist/internal/color/harmony.d.ts +0 -12
- package/dist/internal/color/harmony.d.ts.map +0 -1
- package/dist/internal/color/harmony.js +0 -69
- package/dist/internal/color/harmony.js.map +0 -1
- package/dist/internal/color/hue.d.ts +0 -3
- package/dist/internal/color/hue.d.ts.map +0 -1
- package/dist/internal/color/hue.js +0 -7
- package/dist/internal/color/hue.js.map +0 -1
- package/dist/internal/color/index.d.ts +0 -10
- package/dist/internal/color/index.d.ts.map +0 -1
- package/dist/internal/color/index.js +0 -10
- package/dist/internal/color/index.js.map +0 -1
- package/dist/internal/color/oklch.d.ts +0 -6
- package/dist/internal/color/oklch.d.ts.map +0 -1
- package/dist/internal/color/oklch.js +0 -50
- package/dist/internal/color/oklch.js.map +0 -1
- package/dist/internal/color/primary.d.ts +0 -3
- package/dist/internal/color/primary.d.ts.map +0 -1
- package/dist/internal/color/primary.js +0 -44
- package/dist/internal/color/primary.js.map +0 -1
- package/dist/internal/color/roleHues.d.ts +0 -15
- package/dist/internal/color/roleHues.d.ts.map +0 -1
- package/dist/internal/color/roleHues.js +0 -103
- package/dist/internal/color/roleHues.js.map +0 -1
- package/dist/internal/color/roleScales.d.ts +0 -20
- package/dist/internal/color/roleScales.d.ts.map +0 -1
- package/dist/internal/color/roleScales.js +0 -79
- package/dist/internal/color/roleScales.js.map +0 -1
- package/dist/internal/color/scales.d.ts +0 -19
- package/dist/internal/color/scales.d.ts.map +0 -1
- package/dist/internal/color/scales.js +0 -135
- package/dist/internal/color/scales.js.map +0 -1
- package/dist/internal/color/semanticTokens.d.ts +0 -28
- package/dist/internal/color/semanticTokens.d.ts.map +0 -1
- package/dist/internal/color/semanticTokens.js +0 -84
- package/dist/internal/color/semanticTokens.js.map +0 -1
- package/dist/internal/color/types.d.ts +0 -10
- package/dist/internal/color/types.d.ts.map +0 -1
- package/dist/internal/color/types.js +0 -4
- package/dist/internal/color/types.js.map +0 -1
- package/dist/patterns/theme-composer/recommendations.d.ts +0 -14
- package/dist/patterns/theme-composer/recommendations.d.ts.map +0 -1
- package/dist/patterns/theme-composer/recommendations.js +0 -58
- package/dist/patterns/theme-composer/recommendations.js.map +0 -1
- package/src/internal/color/colorToneRecipes.test.ts +0 -89
- package/src/internal/color/colorToneRecipes.ts +0 -167
- package/src/internal/color/harmony.test.ts +0 -145
- package/src/internal/color/harmony.ts +0 -96
- package/src/internal/color/hue.test.ts +0 -28
- package/src/internal/color/hue.ts +0 -7
- package/src/internal/color/index.ts +0 -44
- package/src/internal/color/oklch.ts +0 -65
- package/src/internal/color/primary.test.ts +0 -105
- package/src/internal/color/primary.ts +0 -64
- package/src/internal/color/roleHues.test.ts +0 -197
- package/src/internal/color/roleHues.ts +0 -142
- package/src/internal/color/roleScales.test.ts +0 -220
- package/src/internal/color/roleScales.ts +0 -127
- package/src/internal/color/scales.test.ts +0 -151
- package/src/internal/color/scales.ts +0 -194
- package/src/internal/color/semanticTokens.test.ts +0 -170
- package/src/internal/color/semanticTokens.ts +0 -114
- package/src/internal/color/types.ts +0 -15
- package/src/patterns/theme-composer/recommendations.ts +0 -85
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { parseHexToOklch } from './oklch';
|
|
2
|
-
import { getZoraRoleColorScale } from './roleScales';
|
|
3
|
-
/**
|
|
4
|
-
* Selects the more readable of two candidate hex colors against a given background,
|
|
5
|
-
* using OKLCH lightness as a simple contrast proxy.
|
|
6
|
-
*/
|
|
7
|
-
export function getReadableTextColor(background, candidates) {
|
|
8
|
-
const bgL = parseHexToOklch(background).l;
|
|
9
|
-
const [a, b] = candidates;
|
|
10
|
-
const diffA = Math.abs(parseHexToOklch(a).l - bgL);
|
|
11
|
-
const diffB = Math.abs(parseHexToOklch(b).l - bgL);
|
|
12
|
-
return diffA >= diffB ? a : b;
|
|
13
|
-
}
|
|
14
|
-
export function createZoraSemanticColorTokens(options) {
|
|
15
|
-
// colorTone is accepted now and reserved for future per-tone step overrides
|
|
16
|
-
// (e.g. obsidian/pastel may shift surface selections differently).
|
|
17
|
-
const { roleScales, mode } = options;
|
|
18
|
-
const neutral = getZoraRoleColorScale(roleScales, 'neutral').scale;
|
|
19
|
-
const surfaceTintScale = getZoraRoleColorScale(roleScales, 'surfaceTint').scale;
|
|
20
|
-
const primaryScale = getZoraRoleColorScale(roleScales, 'primary').scale;
|
|
21
|
-
const secondaryScale = getZoraRoleColorScale(roleScales, 'secondary').scale;
|
|
22
|
-
const accentScale = getZoraRoleColorScale(roleScales, 'accent').scale;
|
|
23
|
-
const highlightScale = getZoraRoleColorScale(roleScales, 'highlight').scale;
|
|
24
|
-
if (mode === 'light') {
|
|
25
|
-
const background = neutral[50];
|
|
26
|
-
const surface = neutral[100];
|
|
27
|
-
const surfaceRaised = neutral[50];
|
|
28
|
-
const surfaceTint = surfaceTintScale[100];
|
|
29
|
-
const border = neutral[200];
|
|
30
|
-
const text = neutral[900];
|
|
31
|
-
const textMuted = neutral[700];
|
|
32
|
-
const primary = primaryScale[600];
|
|
33
|
-
const secondary = secondaryScale[600];
|
|
34
|
-
const accent = accentScale[600];
|
|
35
|
-
const highlight = highlightScale[600];
|
|
36
|
-
const onPrimary = getReadableTextColor(primary, [neutral[50], neutral[950]]);
|
|
37
|
-
const onAccent = getReadableTextColor(accent, [neutral[50], neutral[950]]);
|
|
38
|
-
return {
|
|
39
|
-
background,
|
|
40
|
-
surface,
|
|
41
|
-
surfaceRaised,
|
|
42
|
-
surfaceTint,
|
|
43
|
-
border,
|
|
44
|
-
text,
|
|
45
|
-
textMuted,
|
|
46
|
-
primary,
|
|
47
|
-
secondary,
|
|
48
|
-
accent,
|
|
49
|
-
highlight,
|
|
50
|
-
onPrimary,
|
|
51
|
-
onAccent,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
// dark mode
|
|
55
|
-
const background = neutral[950];
|
|
56
|
-
const surface = neutral[900];
|
|
57
|
-
const surfaceRaised = neutral[800];
|
|
58
|
-
const surfaceTint = surfaceTintScale[900];
|
|
59
|
-
const border = neutral[700];
|
|
60
|
-
const text = neutral[50];
|
|
61
|
-
const textMuted = neutral[300];
|
|
62
|
-
const primary = primaryScale[400];
|
|
63
|
-
const secondary = secondaryScale[400];
|
|
64
|
-
const accent = accentScale[400];
|
|
65
|
-
const highlight = highlightScale[400];
|
|
66
|
-
const onPrimary = getReadableTextColor(primary, [neutral[50], neutral[950]]);
|
|
67
|
-
const onAccent = getReadableTextColor(accent, [neutral[50], neutral[950]]);
|
|
68
|
-
return {
|
|
69
|
-
background,
|
|
70
|
-
surface,
|
|
71
|
-
surfaceRaised,
|
|
72
|
-
surfaceTint,
|
|
73
|
-
border,
|
|
74
|
-
text,
|
|
75
|
-
textMuted,
|
|
76
|
-
primary,
|
|
77
|
-
secondary,
|
|
78
|
-
accent,
|
|
79
|
-
highlight,
|
|
80
|
-
onPrimary,
|
|
81
|
-
onAccent,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
//# sourceMappingURL=semanticTokens.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"semanticTokens.js","sourceRoot":"","sources":["../../../src/internal/color/semanticTokens.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ZoraColorTone, ZoraHexColor, ZoraThemeMode } from '../../theme/types';\nimport { parseHexToOklch } from './oklch';\nimport { getZoraRoleColorScale, type ZoraComputedRoleColorScales } from './roleScales';\n\nexport interface ZoraSemanticColorTokens {\n background: ZoraHexColor;\n surface: ZoraHexColor;\n surfaceRaised: ZoraHexColor;\n surfaceTint: ZoraHexColor;\n border: ZoraHexColor;\n text: ZoraHexColor;\n textMuted: ZoraHexColor;\n primary: ZoraHexColor;\n secondary: ZoraHexColor;\n accent: ZoraHexColor;\n highlight: ZoraHexColor;\n onPrimary: ZoraHexColor;\n onAccent: ZoraHexColor;\n}\n\n/**\n * Selects the more readable of two candidate hex colors against a given background,\n * using OKLCH lightness as a simple contrast proxy.\n */\nexport function getReadableTextColor(\n background: ZoraHexColor,\n candidates: readonly [ZoraHexColor, ZoraHexColor],\n): ZoraHexColor {\n const bgL = parseHexToOklch(background).l;\n const [a, b] = candidates;\n const diffA = Math.abs(parseHexToOklch(a).l - bgL);\n const diffB = Math.abs(parseHexToOklch(b).l - bgL);\n return diffA >= diffB ? a : b;\n}\n\nexport function createZoraSemanticColorTokens(options: {\n roleScales: ZoraComputedRoleColorScales;\n mode: ZoraThemeMode;\n colorTone: ZoraColorTone;\n}): ZoraSemanticColorTokens {\n // colorTone is accepted now and reserved for future per-tone step overrides\n // (e.g. obsidian/pastel may shift surface selections differently).\n const { roleScales, mode } = options;\n\n const neutral = getZoraRoleColorScale(roleScales, 'neutral').scale;\n const surfaceTintScale = getZoraRoleColorScale(roleScales, 'surfaceTint').scale;\n const primaryScale = getZoraRoleColorScale(roleScales, 'primary').scale;\n const secondaryScale = getZoraRoleColorScale(roleScales, 'secondary').scale;\n const accentScale = getZoraRoleColorScale(roleScales, 'accent').scale;\n const highlightScale = getZoraRoleColorScale(roleScales, 'highlight').scale;\n\n if (mode === 'light') {\n const background = neutral[50];\n const surface = neutral[100];\n const surfaceRaised = neutral[50];\n const surfaceTint = surfaceTintScale[100];\n const border = neutral[200];\n const text = neutral[900];\n const textMuted = neutral[700];\n const primary = primaryScale[600];\n const secondary = secondaryScale[600];\n const accent = accentScale[600];\n const highlight = highlightScale[600];\n const onPrimary = getReadableTextColor(primary, [neutral[50], neutral[950]]);\n const onAccent = getReadableTextColor(accent, [neutral[50], neutral[950]]);\n\n return {\n background,\n surface,\n surfaceRaised,\n surfaceTint,\n border,\n text,\n textMuted,\n primary,\n secondary,\n accent,\n highlight,\n onPrimary,\n onAccent,\n };\n }\n\n // dark mode\n const background = neutral[950];\n const surface = neutral[900];\n const surfaceRaised = neutral[800];\n const surfaceTint = surfaceTintScale[900];\n const border = neutral[700];\n const text = neutral[50];\n const textMuted = neutral[300];\n const primary = primaryScale[400];\n const secondary = secondaryScale[400];\n const accent = accentScale[400];\n const highlight = highlightScale[400];\n const onPrimary = getReadableTextColor(primary, [neutral[50], neutral[950]]);\n const onAccent = getReadableTextColor(accent, [neutral[50], neutral[950]]);\n\n return {\n background,\n surface,\n surfaceRaised,\n surfaceTint,\n border,\n text,\n textMuted,\n primary,\n secondary,\n accent,\n highlight,\n onPrimary,\n onAccent,\n };\n}\n"]}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { ZoraHexColor } from '../../theme/types';
|
|
2
|
-
export interface ZoraOklchColor {
|
|
3
|
-
l: number;
|
|
4
|
-
c: number;
|
|
5
|
-
h: number;
|
|
6
|
-
}
|
|
7
|
-
export declare const ZORA_COLOR_SCALE_STEPS: readonly [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
|
|
8
|
-
export type ZoraColorScaleStep = (typeof ZORA_COLOR_SCALE_STEPS)[number];
|
|
9
|
-
export type ZoraColorScale = Record<ZoraColorScaleStep, ZoraHexColor>;
|
|
10
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/internal/color/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,eAAO,MAAM,sBAAsB,iEAEzB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,kBAAkB,EAAE,YAAY,CAAC,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/internal/color/types.ts"],"names":[],"mappings":"AAQA,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG;CAC5C,CAAC","sourcesContent":["import type { ZoraHexColor } from '../../theme/types';\n\nexport interface ZoraOklchColor {\n l: number;\n c: number;\n h: number;\n}\n\nexport const ZORA_COLOR_SCALE_STEPS = [\n 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950,\n] as const;\n\nexport type ZoraColorScaleStep = (typeof ZORA_COLOR_SCALE_STEPS)[number];\n\nexport type ZoraColorScale = Record<ZoraColorScaleStep, ZoraHexColor>;\n"]}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { ZoraHexColor, ZoraTheme } from '../../theme/types';
|
|
2
|
-
import type { ThemeComposerAppCategory, ThemeComposerAppMood, ThemeComposerRecommendation } from './types';
|
|
3
|
-
export declare function findThemeComposerRecommendation(options: {
|
|
4
|
-
appCategory?: ThemeComposerAppCategory;
|
|
5
|
-
appMood?: ThemeComposerAppMood;
|
|
6
|
-
recommendations?: readonly ThemeComposerRecommendation[];
|
|
7
|
-
}): ThemeComposerRecommendation | undefined;
|
|
8
|
-
export declare function formatThemeComposerLabel(value: string): string;
|
|
9
|
-
export declare function hueDegreesToZoraHexColor(hueDegrees: number): ZoraHexColor;
|
|
10
|
-
export declare function createThemeFromThemeComposerRecommendation(options: {
|
|
11
|
-
value: ZoraTheme;
|
|
12
|
-
recommendation: ThemeComposerRecommendation;
|
|
13
|
-
}): ZoraTheme;
|
|
14
|
-
//# sourceMappingURL=recommendations.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recommendations.d.ts","sourceRoot":"","sources":["../../../src/patterns/theme-composer/recommendations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,wBAAgB,+BAA+B,CAAC,OAAO,EAAE;IACvD,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IAC/B,eAAe,CAAC,EAAE,SAAS,2BAA2B,EAAE,CAAC;CAC1D,GAAG,2BAA2B,GAAG,SAAS,CAU1C;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW9D;AAED,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,CA+BzE;AAED,wBAAgB,0CAA0C,CAAC,OAAO,EAAE;IAClE,KAAK,EAAE,SAAS,CAAC;IACjB,cAAc,EAAE,2BAA2B,CAAC;CAC7C,GAAG,SAAS,CAYZ"}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
export function findThemeComposerRecommendation(options) {
|
|
2
|
-
if (options.appCategory === undefined || options.recommendations === undefined) {
|
|
3
|
-
return undefined;
|
|
4
|
-
}
|
|
5
|
-
return options.recommendations.find((recommendation) => recommendation.appCategory === options.appCategory &&
|
|
6
|
-
(options.appMood === undefined || recommendation.appMood === options.appMood));
|
|
7
|
-
}
|
|
8
|
-
export function formatThemeComposerLabel(value) {
|
|
9
|
-
const spaced = value
|
|
10
|
-
.replace(/_/g, ' ')
|
|
11
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
12
|
-
.trim();
|
|
13
|
-
if (spaced.length === 0) {
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
|
-
return `${spaced.slice(0, 1).toUpperCase()}${spaced.slice(1)}`;
|
|
17
|
-
}
|
|
18
|
-
export function hueDegreesToZoraHexColor(hueDegrees) {
|
|
19
|
-
const normalizedHue = ((hueDegrees % 360) + 360) % 360;
|
|
20
|
-
const chroma = 0.56;
|
|
21
|
-
const lightness = 0.46;
|
|
22
|
-
const second = chroma * (1 - Math.abs(((normalizedHue / 60) % 2) - 1));
|
|
23
|
-
const match = lightness - chroma / 2;
|
|
24
|
-
const hueSector = Math.floor(normalizedHue / 60);
|
|
25
|
-
const [redPrime, greenPrime, bluePrime] = (() => {
|
|
26
|
-
switch (hueSector) {
|
|
27
|
-
case 0:
|
|
28
|
-
return [chroma, second, 0];
|
|
29
|
-
case 1:
|
|
30
|
-
return [second, chroma, 0];
|
|
31
|
-
case 2:
|
|
32
|
-
return [0, chroma, second];
|
|
33
|
-
case 3:
|
|
34
|
-
return [0, second, chroma];
|
|
35
|
-
case 4:
|
|
36
|
-
return [second, 0, chroma];
|
|
37
|
-
default:
|
|
38
|
-
return [chroma, 0, second];
|
|
39
|
-
}
|
|
40
|
-
})();
|
|
41
|
-
const toHexChannel = (channel) => {
|
|
42
|
-
const value = Math.round(Math.min(1, Math.max(0, channel + match)) * 255);
|
|
43
|
-
return value.toString(16).padStart(2, '0');
|
|
44
|
-
};
|
|
45
|
-
return `#${toHexChannel(redPrime)}${toHexChannel(greenPrime)}${toHexChannel(bluePrime)}`;
|
|
46
|
-
}
|
|
47
|
-
export function createThemeFromThemeComposerRecommendation(options) {
|
|
48
|
-
const suggestedPrimaryColor = options.recommendation.suggestedPrimaryHueDegrees === undefined
|
|
49
|
-
? options.value.primaryColor
|
|
50
|
-
: hueDegreesToZoraHexColor(options.recommendation.suggestedPrimaryHueDegrees);
|
|
51
|
-
return {
|
|
52
|
-
...options.value,
|
|
53
|
-
primaryColor: suggestedPrimaryColor,
|
|
54
|
-
harmony: options.recommendation.suggestedHarmony,
|
|
55
|
-
colorTone: options.recommendation.suggestedColorTone,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
//# sourceMappingURL=recommendations.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recommendations.js","sourceRoot":"","sources":["../../../src/patterns/theme-composer/recommendations.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ZoraHexColor, ZoraTheme } from '../../theme/types';\nimport type {\n ThemeComposerAppCategory,\n ThemeComposerAppMood,\n ThemeComposerRecommendation,\n} from './types';\n\nexport function findThemeComposerRecommendation(options: {\n appCategory?: ThemeComposerAppCategory;\n appMood?: ThemeComposerAppMood;\n recommendations?: readonly ThemeComposerRecommendation[];\n}): ThemeComposerRecommendation | undefined {\n if (options.appCategory === undefined || options.recommendations === undefined) {\n return undefined;\n }\n\n return options.recommendations.find(\n (recommendation) =>\n recommendation.appCategory === options.appCategory &&\n (options.appMood === undefined || recommendation.appMood === options.appMood),\n );\n}\n\nexport function formatThemeComposerLabel(value: string): string {\n const spaced = value\n .replace(/_/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .trim();\n\n if (spaced.length === 0) {\n return value;\n }\n\n return `${spaced.slice(0, 1).toUpperCase()}${spaced.slice(1)}`;\n}\n\nexport function hueDegreesToZoraHexColor(hueDegrees: number): ZoraHexColor {\n const normalizedHue = ((hueDegrees % 360) + 360) % 360;\n const chroma = 0.56;\n const lightness = 0.46;\n const second = chroma * (1 - Math.abs(((normalizedHue / 60) % 2) - 1));\n const match = lightness - chroma / 2;\n\n const hueSector = Math.floor(normalizedHue / 60);\n const [redPrime, greenPrime, bluePrime] = (() => {\n switch (hueSector) {\n case 0:\n return [chroma, second, 0] as const;\n case 1:\n return [second, chroma, 0] as const;\n case 2:\n return [0, chroma, second] as const;\n case 3:\n return [0, second, chroma] as const;\n case 4:\n return [second, 0, chroma] as const;\n default:\n return [chroma, 0, second] as const;\n }\n })();\n\n const toHexChannel = (channel: number): string => {\n const value = Math.round(Math.min(1, Math.max(0, channel + match)) * 255);\n return value.toString(16).padStart(2, '0');\n };\n\n return `#${toHexChannel(redPrime)}${toHexChannel(greenPrime)}${toHexChannel(bluePrime)}`;\n}\n\nexport function createThemeFromThemeComposerRecommendation(options: {\n value: ZoraTheme;\n recommendation: ThemeComposerRecommendation;\n}): ZoraTheme {\n const suggestedPrimaryColor =\n options.recommendation.suggestedPrimaryHueDegrees === undefined\n ? options.value.primaryColor\n : hueDegreesToZoraHexColor(options.recommendation.suggestedPrimaryHueDegrees);\n\n return {\n ...options.value,\n primaryColor: suggestedPrimaryColor,\n harmony: options.recommendation.suggestedHarmony,\n colorTone: options.recommendation.suggestedColorTone,\n };\n}\n"]}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { ZORA_COLOR_TONES } from '../../theme/types';
|
|
4
|
-
import {
|
|
5
|
-
getZoraColorToneRecipe,
|
|
6
|
-
getZoraColorToneRoleChromaFactor,
|
|
7
|
-
type ZoraColorToneRecipe,
|
|
8
|
-
type ZoraHueScaleRoleId,
|
|
9
|
-
} from './index';
|
|
10
|
-
|
|
11
|
-
const HUE_BACKED_ROLES: readonly ZoraHueScaleRoleId[] = [
|
|
12
|
-
'primary',
|
|
13
|
-
'secondary',
|
|
14
|
-
'accent',
|
|
15
|
-
'highlight',
|
|
16
|
-
'surfaceTint',
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
function expectFinitePositive(value: number) {
|
|
20
|
-
expect(Number.isFinite(value)).toBe(true);
|
|
21
|
-
expect(value).toBeGreaterThan(0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function expectSaneRecipe(recipe: ZoraColorToneRecipe) {
|
|
25
|
-
expectFinitePositive(recipe.maxChroma);
|
|
26
|
-
expectFinitePositive(recipe.minMidChroma);
|
|
27
|
-
expect(recipe.minMidChroma).toBeLessThan(recipe.maxChroma);
|
|
28
|
-
|
|
29
|
-
for (const role of HUE_BACKED_ROLES) {
|
|
30
|
-
expectFinitePositive(getZoraColorToneRoleChromaFactor(recipe, role));
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('color tone recipes', () => {
|
|
35
|
-
test('every ZORA color tone has a recipe', () => {
|
|
36
|
-
for (const colorTone of ZORA_COLOR_TONES) {
|
|
37
|
-
expect(getZoraColorToneRecipe(colorTone).colorTone).toBe(colorTone);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('every recipe has sane chroma values and all hue-backed role factors', () => {
|
|
42
|
-
for (const colorTone of ZORA_COLOR_TONES) {
|
|
43
|
-
expectSaneRecipe(getZoraColorToneRecipe(colorTone));
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('records the initial background and foreground lane pairings', () => {
|
|
48
|
-
expect(getZoraColorToneRecipe('fluorescent').laneRecipe).toEqual({
|
|
49
|
-
backgroundTone: 'obsidian',
|
|
50
|
-
foregroundTone: 'fluorescent',
|
|
51
|
-
});
|
|
52
|
-
expect(getZoraColorToneRecipe('obsidian').laneRecipe).toEqual({
|
|
53
|
-
backgroundTone: 'obsidian',
|
|
54
|
-
foregroundTone: 'fluorescent',
|
|
55
|
-
});
|
|
56
|
-
expect(getZoraColorToneRecipe('pastel').laneRecipe).toEqual({
|
|
57
|
-
backgroundTone: 'pastel',
|
|
58
|
-
foregroundTone: 'jewel',
|
|
59
|
-
});
|
|
60
|
-
expect(getZoraColorToneRecipe('earth').laneRecipe).toEqual({
|
|
61
|
-
backgroundTone: 'earth',
|
|
62
|
-
foregroundTone: 'mineral',
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('surface tint remains lower intensity than foreground action roles', () => {
|
|
67
|
-
for (const colorTone of ZORA_COLOR_TONES) {
|
|
68
|
-
const recipe = getZoraColorToneRecipe(colorTone);
|
|
69
|
-
const surfaceTint = getZoraColorToneRoleChromaFactor(recipe, 'surfaceTint');
|
|
70
|
-
|
|
71
|
-
expect(surfaceTint).toBeLessThan(getZoraColorToneRoleChromaFactor(recipe, 'primary'));
|
|
72
|
-
expect(surfaceTint).toBeLessThan(getZoraColorToneRoleChromaFactor(recipe, 'accent'));
|
|
73
|
-
expect(surfaceTint).toBeLessThan(getZoraColorToneRoleChromaFactor(recipe, 'highlight'));
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('high-energy tones allow higher chroma than neutral and pastel', () => {
|
|
78
|
-
expect(getZoraColorToneRecipe('fluorescent').maxChroma).toBeGreaterThan(
|
|
79
|
-
getZoraColorToneRecipe('neutral').maxChroma,
|
|
80
|
-
);
|
|
81
|
-
expect(getZoraColorToneRecipe('fluorescent').maxChroma).toBeGreaterThan(
|
|
82
|
-
getZoraColorToneRecipe('pastel').maxChroma,
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('getter is deterministic', () => {
|
|
87
|
-
expect(getZoraColorToneRecipe('jewel')).toEqual(getZoraColorToneRecipe('jewel'));
|
|
88
|
-
});
|
|
89
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import type { ZoraColorTone } from '../../theme/types';
|
|
2
|
-
import type { ZoraHueScaleRoleId } from './scales';
|
|
3
|
-
|
|
4
|
-
export interface ZoraColorToneLaneRecipe {
|
|
5
|
-
backgroundTone: ZoraColorTone;
|
|
6
|
-
foregroundTone: ZoraColorTone;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ZoraColorToneRoleChromaFactors {
|
|
10
|
-
primary: number;
|
|
11
|
-
secondary: number;
|
|
12
|
-
accent: number;
|
|
13
|
-
highlight: number;
|
|
14
|
-
surfaceTint: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ZoraColorToneRecipe {
|
|
18
|
-
colorTone: ZoraColorTone;
|
|
19
|
-
laneRecipe: ZoraColorToneLaneRecipe;
|
|
20
|
-
roleChromaFactors: ZoraColorToneRoleChromaFactors;
|
|
21
|
-
maxChroma: number;
|
|
22
|
-
minMidChroma: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const ZORA_COLOR_TONE_RECIPES = {
|
|
26
|
-
neutral: {
|
|
27
|
-
colorTone: 'neutral',
|
|
28
|
-
laneRecipe: { backgroundTone: 'neutral', foregroundTone: 'jewel' },
|
|
29
|
-
roleChromaFactors: {
|
|
30
|
-
primary: 0.72,
|
|
31
|
-
secondary: 0.48,
|
|
32
|
-
accent: 0.56,
|
|
33
|
-
highlight: 0.6,
|
|
34
|
-
surfaceTint: 0.12,
|
|
35
|
-
},
|
|
36
|
-
maxChroma: 0.14,
|
|
37
|
-
minMidChroma: 0.025,
|
|
38
|
-
},
|
|
39
|
-
pastel: {
|
|
40
|
-
colorTone: 'pastel',
|
|
41
|
-
laneRecipe: { backgroundTone: 'pastel', foregroundTone: 'jewel' },
|
|
42
|
-
roleChromaFactors: {
|
|
43
|
-
primary: 0.58,
|
|
44
|
-
secondary: 0.48,
|
|
45
|
-
accent: 0.55,
|
|
46
|
-
highlight: 0.62,
|
|
47
|
-
surfaceTint: 0.2,
|
|
48
|
-
},
|
|
49
|
-
maxChroma: 0.12,
|
|
50
|
-
minMidChroma: 0.02,
|
|
51
|
-
},
|
|
52
|
-
earth: {
|
|
53
|
-
colorTone: 'earth',
|
|
54
|
-
laneRecipe: { backgroundTone: 'earth', foregroundTone: 'mineral' },
|
|
55
|
-
roleChromaFactors: {
|
|
56
|
-
primary: 0.64,
|
|
57
|
-
secondary: 0.52,
|
|
58
|
-
accent: 0.58,
|
|
59
|
-
highlight: 0.6,
|
|
60
|
-
surfaceTint: 0.16,
|
|
61
|
-
},
|
|
62
|
-
maxChroma: 0.13,
|
|
63
|
-
minMidChroma: 0.022,
|
|
64
|
-
},
|
|
65
|
-
mineral: {
|
|
66
|
-
colorTone: 'mineral',
|
|
67
|
-
laneRecipe: { backgroundTone: 'mineral', foregroundTone: 'jewel' },
|
|
68
|
-
roleChromaFactors: {
|
|
69
|
-
primary: 0.7,
|
|
70
|
-
secondary: 0.56,
|
|
71
|
-
accent: 0.64,
|
|
72
|
-
highlight: 0.68,
|
|
73
|
-
surfaceTint: 0.16,
|
|
74
|
-
},
|
|
75
|
-
maxChroma: 0.14,
|
|
76
|
-
minMidChroma: 0.025,
|
|
77
|
-
},
|
|
78
|
-
muted: {
|
|
79
|
-
colorTone: 'muted',
|
|
80
|
-
laneRecipe: { backgroundTone: 'muted', foregroundTone: 'jewel' },
|
|
81
|
-
roleChromaFactors: {
|
|
82
|
-
primary: 0.6,
|
|
83
|
-
secondary: 0.5,
|
|
84
|
-
accent: 0.56,
|
|
85
|
-
highlight: 0.6,
|
|
86
|
-
surfaceTint: 0.14,
|
|
87
|
-
},
|
|
88
|
-
maxChroma: 0.12,
|
|
89
|
-
minMidChroma: 0.02,
|
|
90
|
-
},
|
|
91
|
-
jewel: {
|
|
92
|
-
colorTone: 'jewel',
|
|
93
|
-
laneRecipe: { backgroundTone: 'neutral', foregroundTone: 'jewel' },
|
|
94
|
-
roleChromaFactors: {
|
|
95
|
-
primary: 1,
|
|
96
|
-
secondary: 0.72,
|
|
97
|
-
accent: 0.85,
|
|
98
|
-
highlight: 1,
|
|
99
|
-
surfaceTint: 0.18,
|
|
100
|
-
},
|
|
101
|
-
maxChroma: 0.2,
|
|
102
|
-
minMidChroma: 0.04,
|
|
103
|
-
},
|
|
104
|
-
fluorescent: {
|
|
105
|
-
colorTone: 'fluorescent',
|
|
106
|
-
laneRecipe: { backgroundTone: 'obsidian', foregroundTone: 'fluorescent' },
|
|
107
|
-
roleChromaFactors: {
|
|
108
|
-
primary: 1.12,
|
|
109
|
-
secondary: 0.82,
|
|
110
|
-
accent: 1.05,
|
|
111
|
-
highlight: 1.18,
|
|
112
|
-
surfaceTint: 0.22,
|
|
113
|
-
},
|
|
114
|
-
maxChroma: 0.24,
|
|
115
|
-
minMidChroma: 0.045,
|
|
116
|
-
},
|
|
117
|
-
obsidian: {
|
|
118
|
-
colorTone: 'obsidian',
|
|
119
|
-
laneRecipe: { backgroundTone: 'obsidian', foregroundTone: 'fluorescent' },
|
|
120
|
-
roleChromaFactors: {
|
|
121
|
-
primary: 1.08,
|
|
122
|
-
secondary: 0.78,
|
|
123
|
-
accent: 1,
|
|
124
|
-
highlight: 1.12,
|
|
125
|
-
surfaceTint: 0.2,
|
|
126
|
-
},
|
|
127
|
-
maxChroma: 0.22,
|
|
128
|
-
minMidChroma: 0.04,
|
|
129
|
-
},
|
|
130
|
-
vaporwave: {
|
|
131
|
-
colorTone: 'vaporwave',
|
|
132
|
-
laneRecipe: { backgroundTone: 'pastel', foregroundTone: 'fluorescent' },
|
|
133
|
-
roleChromaFactors: {
|
|
134
|
-
primary: 0.95,
|
|
135
|
-
secondary: 0.72,
|
|
136
|
-
accent: 1,
|
|
137
|
-
highlight: 1.08,
|
|
138
|
-
surfaceTint: 0.24,
|
|
139
|
-
},
|
|
140
|
-
maxChroma: 0.2,
|
|
141
|
-
minMidChroma: 0.035,
|
|
142
|
-
},
|
|
143
|
-
monochromeAccent: {
|
|
144
|
-
colorTone: 'monochromeAccent',
|
|
145
|
-
laneRecipe: { backgroundTone: 'neutral', foregroundTone: 'jewel' },
|
|
146
|
-
roleChromaFactors: {
|
|
147
|
-
primary: 0.88,
|
|
148
|
-
secondary: 0.36,
|
|
149
|
-
accent: 0.95,
|
|
150
|
-
highlight: 0.88,
|
|
151
|
-
surfaceTint: 0.08,
|
|
152
|
-
},
|
|
153
|
-
maxChroma: 0.18,
|
|
154
|
-
minMidChroma: 0.035,
|
|
155
|
-
},
|
|
156
|
-
} satisfies Record<ZoraColorTone, ZoraColorToneRecipe>;
|
|
157
|
-
|
|
158
|
-
export function getZoraColorToneRecipe(colorTone: ZoraColorTone): ZoraColorToneRecipe {
|
|
159
|
-
return ZORA_COLOR_TONE_RECIPES[colorTone];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function getZoraColorToneRoleChromaFactor(
|
|
163
|
-
recipe: ZoraColorToneRecipe,
|
|
164
|
-
role: ZoraHueScaleRoleId,
|
|
165
|
-
): number {
|
|
166
|
-
return recipe.roleChromaFactors[role];
|
|
167
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import type { ZoraHexColor } from '../../theme/types';
|
|
4
|
-
import { normalizeHueDegrees } from './hue';
|
|
5
|
-
import {
|
|
6
|
-
computeZoraHarmony,
|
|
7
|
-
parseHexToOklch,
|
|
8
|
-
type ZoraComputedHarmony,
|
|
9
|
-
type ZoraHarmonySlot,
|
|
10
|
-
type ZoraHarmonySlotId,
|
|
11
|
-
} from './index';
|
|
12
|
-
|
|
13
|
-
function hueDeltaDegrees(a: number, b: number): number {
|
|
14
|
-
const raw = Math.abs(a - b) % 360;
|
|
15
|
-
return Math.min(raw, 360 - raw);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function expectNormalizedHue(hue: number) {
|
|
19
|
-
expect(hue).toBeGreaterThanOrEqual(0);
|
|
20
|
-
expect(hue).toBeLessThan(360);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function getSlotIds(slots: readonly ZoraHarmonySlot[]): ZoraHarmonySlotId[] {
|
|
24
|
-
return slots.map((slot) => slot.id);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('computeZoraHarmony', () => {
|
|
28
|
-
test('monochromatic returns 1 slot', () => {
|
|
29
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
30
|
-
const harmony: ZoraComputedHarmony = computeZoraHarmony(seed, 'monochromatic');
|
|
31
|
-
|
|
32
|
-
expect(harmony.orderedSlots).toHaveLength(1);
|
|
33
|
-
expect(harmony.orderedSlots[0].id).toBe('base');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('complementary returns 2 slots', () => {
|
|
37
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
38
|
-
const harmony: ZoraComputedHarmony = computeZoraHarmony(seed, 'complementary');
|
|
39
|
-
|
|
40
|
-
expect(harmony.orderedSlots).toHaveLength(2);
|
|
41
|
-
expect(harmony.orderedSlots[0].id).toBe('base');
|
|
42
|
-
expect(harmony.orderedSlots[1].id).toBe('a');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('analogous returns 3 slots', () => {
|
|
46
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
47
|
-
const harmony: ZoraComputedHarmony = computeZoraHarmony(seed, 'analogous');
|
|
48
|
-
|
|
49
|
-
expect(harmony.orderedSlots).toHaveLength(3);
|
|
50
|
-
expect(getSlotIds(harmony.orderedSlots)).toEqual(['base', 'a', 'b']);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('splitComplementary returns 3 slots', () => {
|
|
54
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
55
|
-
const harmony: ZoraComputedHarmony = computeZoraHarmony(seed, 'splitComplementary');
|
|
56
|
-
|
|
57
|
-
expect(harmony.orderedSlots).toHaveLength(3);
|
|
58
|
-
expect(getSlotIds(harmony.orderedSlots)).toEqual(['base', 'a', 'b']);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('triadic returns 3 slots', () => {
|
|
62
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
63
|
-
const harmony: ZoraComputedHarmony = computeZoraHarmony(seed, 'triadic');
|
|
64
|
-
|
|
65
|
-
expect(harmony.orderedSlots).toHaveLength(3);
|
|
66
|
-
expect(getSlotIds(harmony.orderedSlots)).toEqual(['base', 'a', 'b']);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('tetradic returns 4 slots', () => {
|
|
70
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
71
|
-
const harmony: ZoraComputedHarmony = computeZoraHarmony(seed, 'tetradic');
|
|
72
|
-
|
|
73
|
-
expect(harmony.orderedSlots).toHaveLength(4);
|
|
74
|
-
expect(getSlotIds(harmony.orderedSlots)).toEqual(['base', 'a', 'b', 'c']);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('every computed hue is normalized to [0, 360)', () => {
|
|
78
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
79
|
-
const harmonies = [
|
|
80
|
-
'monochromatic',
|
|
81
|
-
'complementary',
|
|
82
|
-
'analogous',
|
|
83
|
-
'splitComplementary',
|
|
84
|
-
'triadic',
|
|
85
|
-
'tetradic',
|
|
86
|
-
] as const;
|
|
87
|
-
|
|
88
|
-
for (const kind of harmonies) {
|
|
89
|
-
const computed = computeZoraHarmony(seed, kind);
|
|
90
|
-
for (const slot of computed.orderedSlots) {
|
|
91
|
-
expectNormalizedHue(slot.hue);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('base slot approximately matches parsed seed hue for a non-neutral seed', () => {
|
|
97
|
-
const seed: ZoraHexColor = '#ff00ff';
|
|
98
|
-
const seedOklch = parseHexToOklch(seed);
|
|
99
|
-
|
|
100
|
-
const computed = computeZoraHarmony(seed, 'complementary');
|
|
101
|
-
const [base] = computed.orderedSlots;
|
|
102
|
-
|
|
103
|
-
expect(base).toBeDefined();
|
|
104
|
-
if (!base) {
|
|
105
|
-
throw new Error('Expected a base harmony slot.');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
expect(base.id).toBe('base');
|
|
109
|
-
expect(hueDeltaDegrees(seedOklch.h, base.hue)).toBeLessThan(0.5);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('neutral/low-chroma seed uses a stable fallback hue', () => {
|
|
113
|
-
const seed: ZoraHexColor = '#808080';
|
|
114
|
-
const computed = computeZoraHarmony(seed, 'monochromatic');
|
|
115
|
-
|
|
116
|
-
expect(computed.orderedSlots).toHaveLength(1);
|
|
117
|
-
expect(computed.orderedSlots[0].hue).toBe(260);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('is deterministic for the same seed/harmony', () => {
|
|
121
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
122
|
-
|
|
123
|
-
expect(computeZoraHarmony(seed, 'triadic')).toEqual(computeZoraHarmony(seed, 'triadic'));
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
test('complementary slots are based on base hue + 180', () => {
|
|
127
|
-
const seed: ZoraHexColor = '#0f766e';
|
|
128
|
-
const computed = computeZoraHarmony(seed, 'complementary');
|
|
129
|
-
const [base, a] = computed.orderedSlots;
|
|
130
|
-
|
|
131
|
-
expect(base).toBeDefined();
|
|
132
|
-
expect(a).toBeDefined();
|
|
133
|
-
if (!base || !a) {
|
|
134
|
-
throw new Error('Expected complementary harmony slots.');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
expect(hueDeltaDegrees(a.hue, normalizeHueDegrees(base.hue + 180))).toBeLessThan(0.0001);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test('throws on invalid seed in test/development', () => {
|
|
141
|
-
const invalid: ZoraHexColor = '#nope';
|
|
142
|
-
|
|
143
|
-
expect(() => computeZoraHarmony(invalid, 'triadic')).toThrow();
|
|
144
|
-
});
|
|
145
|
-
});
|