@ankhorage/zora 0.11.0 → 0.12.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 +12 -0
- package/README.md +16 -0
- package/dist/internal/color/harmony.d.ts +12 -0
- package/dist/internal/color/harmony.d.ts.map +1 -0
- package/dist/internal/color/harmony.js +69 -0
- package/dist/internal/color/harmony.js.map +1 -0
- package/dist/internal/color/hue.d.ts +3 -0
- package/dist/internal/color/hue.d.ts.map +1 -0
- package/dist/internal/color/hue.js +7 -0
- package/dist/internal/color/hue.js.map +1 -0
- package/dist/internal/color/index.d.ts +6 -0
- package/dist/internal/color/index.d.ts.map +1 -0
- package/dist/internal/color/index.js +6 -0
- package/dist/internal/color/index.js.map +1 -0
- package/dist/internal/color/oklch.d.ts +6 -0
- package/dist/internal/color/oklch.d.ts.map +1 -0
- package/dist/internal/color/oklch.js +50 -0
- package/dist/internal/color/oklch.js.map +1 -0
- package/dist/internal/color/primary.d.ts +3 -0
- package/dist/internal/color/primary.d.ts.map +1 -0
- package/dist/internal/color/primary.js +44 -0
- package/dist/internal/color/primary.js.map +1 -0
- package/dist/internal/color/scales.d.ts +10 -0
- package/dist/internal/color/scales.d.ts.map +1 -0
- package/dist/internal/color/scales.js +110 -0
- package/dist/internal/color/scales.js.map +1 -0
- package/dist/internal/color/types.d.ts +10 -0
- package/dist/internal/color/types.d.ts.map +1 -0
- package/dist/internal/color/types.js +4 -0
- package/dist/internal/color/types.js.map +1 -0
- package/dist/theme/createZoraThemeConfig.d.ts.map +1 -1
- package/dist/theme/createZoraThemeConfig.js +1 -5
- package/dist/theme/createZoraThemeConfig.js.map +1 -1
- package/package.json +4 -2
- package/src/internal/color/harmony.test.ts +145 -0
- package/src/internal/color/harmony.ts +96 -0
- package/src/internal/color/hue.test.ts +28 -0
- package/src/internal/color/hue.ts +7 -0
- package/src/internal/color/index.ts +15 -0
- package/src/internal/color/oklch.ts +65 -0
- package/src/internal/color/primary.test.ts +105 -0
- package/src/internal/color/primary.ts +64 -0
- package/src/internal/color/scales.test.ts +151 -0
- package/src/internal/color/scales.ts +145 -0
- package/src/internal/color/types.ts +15 -0
- package/src/theme/createZoraThemeConfig.test.ts +18 -4
- package/src/theme/createZoraThemeConfig.ts +2 -7
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { ZoraHexColor } from '../../theme/types';
|
|
2
|
+
import { clampOklchToGamut, formatOklchAsHex, parseHexToOklch } from './oklch';
|
|
3
|
+
import { type ZoraColorScale, type ZoraColorScaleStep } from './types';
|
|
4
|
+
|
|
5
|
+
export interface CreateZoraColorScaleOptions {
|
|
6
|
+
seed: ZoraHexColor;
|
|
7
|
+
role?: 'primary' | 'neutral';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const PRIMARY_LIGHTNESS_BY_STEP: Record<ZoraColorScaleStep, number> = {
|
|
11
|
+
50: 0.97,
|
|
12
|
+
100: 0.93,
|
|
13
|
+
200: 0.86,
|
|
14
|
+
300: 0.78,
|
|
15
|
+
400: 0.68,
|
|
16
|
+
500: 0.58,
|
|
17
|
+
600: 0.5,
|
|
18
|
+
700: 0.42,
|
|
19
|
+
800: 0.34,
|
|
20
|
+
900: 0.27,
|
|
21
|
+
950: 0.2,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const NEUTRAL_LIGHTNESS_BY_STEP: Record<ZoraColorScaleStep, number> = {
|
|
25
|
+
50: 0.98,
|
|
26
|
+
100: 0.95,
|
|
27
|
+
200: 0.89,
|
|
28
|
+
300: 0.8,
|
|
29
|
+
400: 0.68,
|
|
30
|
+
500: 0.55,
|
|
31
|
+
600: 0.44,
|
|
32
|
+
700: 0.34,
|
|
33
|
+
800: 0.25,
|
|
34
|
+
900: 0.18,
|
|
35
|
+
950: 0.12,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const PRIMARY_CHROMA_MULTIPLIER_BY_STEP: Record<ZoraColorScaleStep, number> = {
|
|
39
|
+
50: 0.2,
|
|
40
|
+
100: 0.3,
|
|
41
|
+
200: 0.45,
|
|
42
|
+
300: 0.7,
|
|
43
|
+
400: 0.95,
|
|
44
|
+
500: 1,
|
|
45
|
+
600: 0.95,
|
|
46
|
+
700: 0.85,
|
|
47
|
+
800: 0.65,
|
|
48
|
+
900: 0.45,
|
|
49
|
+
950: 0.3,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const MAX_PRIMARY_SCALE_CHROMA = 0.2;
|
|
53
|
+
const MIN_PRIMARY_SCALE_CHROMA = 0.04;
|
|
54
|
+
const NEUTRAL_CHROMA = 0.012;
|
|
55
|
+
const DEFAULT_NEUTRAL_HUE_DEGREES = 260;
|
|
56
|
+
|
|
57
|
+
function clampNumber(value: number, min: number, max: number): number {
|
|
58
|
+
return Math.max(min, Math.min(value, max));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolvePrimaryScaleChroma(seedChroma: number, step: ZoraColorScaleStep): number {
|
|
62
|
+
const cappedSeedChroma = clampNumber(seedChroma, 0, MAX_PRIMARY_SCALE_CHROMA);
|
|
63
|
+
const multiplier = PRIMARY_CHROMA_MULTIPLIER_BY_STEP[step];
|
|
64
|
+
const scaled = cappedSeedChroma * multiplier;
|
|
65
|
+
|
|
66
|
+
const bounded = clampNumber(scaled, 0, MAX_PRIMARY_SCALE_CHROMA);
|
|
67
|
+
const shouldEnforceMin = step >= 300 && step <= 700 && seedChroma >= MIN_PRIMARY_SCALE_CHROMA;
|
|
68
|
+
|
|
69
|
+
return shouldEnforceMin ? Math.max(bounded, MIN_PRIMARY_SCALE_CHROMA) : bounded;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createScaleEntries(options: CreateZoraColorScaleOptions): ZoraColorScale {
|
|
73
|
+
const seed = parseHexToOklch(options.seed);
|
|
74
|
+
|
|
75
|
+
if (options.role === 'neutral') {
|
|
76
|
+
const hue = typeof seed.h === 'number' ? seed.h : DEFAULT_NEUTRAL_HUE_DEGREES;
|
|
77
|
+
|
|
78
|
+
return createScaleFromRamp({
|
|
79
|
+
hue,
|
|
80
|
+
chroma: NEUTRAL_CHROMA,
|
|
81
|
+
lightnessByStep: NEUTRAL_LIGHTNESS_BY_STEP,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return createScaleFromRamp({
|
|
86
|
+
hue: seed.h,
|
|
87
|
+
chromaByStep: (step) => resolvePrimaryScaleChroma(seed.c, step),
|
|
88
|
+
lightnessByStep: PRIMARY_LIGHTNESS_BY_STEP,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface CreateScaleFromRampOptions {
|
|
93
|
+
hue: number;
|
|
94
|
+
chroma?: number;
|
|
95
|
+
chromaByStep?: (step: ZoraColorScaleStep) => number;
|
|
96
|
+
lightnessByStep: Record<ZoraColorScaleStep, number>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createScaleColor(
|
|
100
|
+
options: CreateScaleFromRampOptions,
|
|
101
|
+
step: ZoraColorScaleStep,
|
|
102
|
+
): ZoraHexColor {
|
|
103
|
+
const lightness = options.lightnessByStep[step];
|
|
104
|
+
const chroma =
|
|
105
|
+
typeof options.chromaByStep === 'function' ? options.chromaByStep(step) : (options.chroma ?? 0);
|
|
106
|
+
|
|
107
|
+
const clamped = clampOklchToGamut({
|
|
108
|
+
l: clampNumber(lightness, 0, 1),
|
|
109
|
+
c: clampNumber(chroma, 0, 1),
|
|
110
|
+
h: options.hue,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return formatOklchAsHex(clamped);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function createScaleFromRamp(options: CreateScaleFromRampOptions): ZoraColorScale {
|
|
117
|
+
return {
|
|
118
|
+
50: createScaleColor(options, 50),
|
|
119
|
+
100: createScaleColor(options, 100),
|
|
120
|
+
200: createScaleColor(options, 200),
|
|
121
|
+
300: createScaleColor(options, 300),
|
|
122
|
+
400: createScaleColor(options, 400),
|
|
123
|
+
500: createScaleColor(options, 500),
|
|
124
|
+
600: createScaleColor(options, 600),
|
|
125
|
+
700: createScaleColor(options, 700),
|
|
126
|
+
800: createScaleColor(options, 800),
|
|
127
|
+
900: createScaleColor(options, 900),
|
|
128
|
+
950: createScaleColor(options, 950),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createZoraColorScale(options: CreateZoraColorScaleOptions): ZoraColorScale {
|
|
133
|
+
return createScaleEntries({
|
|
134
|
+
seed: options.seed,
|
|
135
|
+
role: options.role,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createZoraPrimaryScale(seed: ZoraHexColor): ZoraColorScale {
|
|
140
|
+
return createZoraColorScale({ seed, role: 'primary' });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createZoraNeutralScale(seed: ZoraHexColor = '#94a3b8'): ZoraColorScale {
|
|
144
|
+
return createZoraColorScale({ seed, role: 'neutral' });
|
|
145
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ZoraHexColor } from '../../theme/types';
|
|
2
|
+
|
|
3
|
+
export interface ZoraOklchColor {
|
|
4
|
+
l: number;
|
|
5
|
+
c: number;
|
|
6
|
+
h: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ZORA_COLOR_SCALE_STEPS = [
|
|
10
|
+
50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950,
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export type ZoraColorScaleStep = (typeof ZORA_COLOR_SCALE_STEPS)[number];
|
|
14
|
+
|
|
15
|
+
export type ZoraColorScale = Record<ZoraColorScaleStep, ZoraHexColor>;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
+
import { parseHexToOklch } from '../internal/color';
|
|
3
4
|
import { createZoraThemeConfig } from './createZoraThemeConfig';
|
|
5
|
+
import type { ZoraHexColor } from './types';
|
|
6
|
+
|
|
7
|
+
function isSixDigitHexColor(value: string): value is ZoraHexColor {
|
|
8
|
+
return /^#[0-9a-f]{6}$/.test(value);
|
|
9
|
+
}
|
|
4
10
|
|
|
5
11
|
describe('createZoraThemeConfig', () => {
|
|
6
12
|
test('converts the default theme seed into a surface config', () => {
|
|
@@ -8,12 +14,18 @@ describe('createZoraThemeConfig', () => {
|
|
|
8
14
|
|
|
9
15
|
expect(themeConfig.id).toBe('zora');
|
|
10
16
|
expect(themeConfig.name).toBe('ZORA');
|
|
11
|
-
expect(themeConfig.light.primaryColor).toBe(
|
|
17
|
+
expect(isSixDigitHexColor(themeConfig.light.primaryColor)).toBe(true);
|
|
12
18
|
expect(themeConfig.light.harmony).toBe('analogous');
|
|
13
19
|
expect(themeConfig.light.systemTone).toBe('jewel');
|
|
14
|
-
expect(themeConfig.dark.primaryColor).toBe(
|
|
20
|
+
expect(isSixDigitHexColor(themeConfig.dark.primaryColor)).toBe(true);
|
|
15
21
|
expect(themeConfig.dark.harmony).toBe('analogous');
|
|
16
22
|
expect(themeConfig.dark.systemTone).toBe('jewel');
|
|
23
|
+
|
|
24
|
+
expect(themeConfig.light.primaryColor).not.toBe(themeConfig.dark.primaryColor);
|
|
25
|
+
|
|
26
|
+
const lightOklch = parseHexToOklch(themeConfig.light.primaryColor);
|
|
27
|
+
const darkOklch = parseHexToOklch(themeConfig.dark.primaryColor);
|
|
28
|
+
expect(darkOklch.l).toBeGreaterThan(lightOklch.l);
|
|
17
29
|
});
|
|
18
30
|
|
|
19
31
|
test('falls back to id when name is omitted', () => {
|
|
@@ -26,11 +38,13 @@ describe('createZoraThemeConfig', () => {
|
|
|
26
38
|
|
|
27
39
|
expect(themeConfig.id).toBe('studio');
|
|
28
40
|
expect(themeConfig.name).toBe('studio');
|
|
29
|
-
expect(themeConfig.light.primaryColor).toBe(
|
|
41
|
+
expect(isSixDigitHexColor(themeConfig.light.primaryColor)).toBe(true);
|
|
30
42
|
expect(themeConfig.light.harmony).toBe('analogous');
|
|
31
43
|
expect(themeConfig.light.systemTone).toBe('jewel');
|
|
32
|
-
expect(themeConfig.dark.primaryColor).toBe(
|
|
44
|
+
expect(isSixDigitHexColor(themeConfig.dark.primaryColor)).toBe(true);
|
|
33
45
|
expect(themeConfig.dark.harmony).toBe('analogous');
|
|
34
46
|
expect(themeConfig.dark.systemTone).toBe('jewel');
|
|
47
|
+
|
|
48
|
+
expect(themeConfig.light.primaryColor).not.toBe(themeConfig.dark.primaryColor);
|
|
35
49
|
});
|
|
36
50
|
});
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import type { ThemeConfig } from '@ankhorage/surface';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { resolveModePrimaryColor } from '../internal/color';
|
|
4
|
+
import type { ZoraTheme } from './types';
|
|
4
5
|
import { zoraDefaultTheme } from './zoraDefaultTheme';
|
|
5
6
|
|
|
6
|
-
function resolveModePrimaryColor(primaryColor: ZoraHexColor, _mode: ZoraThemeMode): ZoraHexColor {
|
|
7
|
-
// Intentionally conservative in Plan 1: mode-specific primary derivation
|
|
8
|
-
// (OKLCH/lightness scale, etc.) comes in later theme-engine work.
|
|
9
|
-
return primaryColor;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
7
|
export function createZoraThemeConfig(theme: ZoraTheme = zoraDefaultTheme): ThemeConfig {
|
|
13
8
|
return {
|
|
14
9
|
id: theme.id,
|