@ankhorage/zora 0.12.2 → 0.12.3

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 2f08e20: Adds internal role color scale generation for future ZORA theme generation work.
8
+
3
9
  ## 0.12.2
4
10
 
5
11
  ### Patch Changes
@@ -2,6 +2,7 @@ export { computeZoraHarmony, type ZoraComputedHarmony, type ZoraHarmonySlot, typ
2
2
  export { parseHexToOklch } from './oklch';
3
3
  export { resolveModePrimaryColor } from './primary';
4
4
  export { assignZoraHarmonyRoleHues, getZoraHueRoleAssignment, type ZoraComputedHueRoles, type ZoraHueRoleAssignment, type ZoraHueRoleId, } from './roleHues';
5
+ export { createZoraRoleColorScales, getZoraRoleColorScale, ZORA_COLOR_SCALE_ROLE_ORDER, type ZoraColorScaleRoleId, type ZoraComputedRoleColorScales, type ZoraRoleColorScale, } from './roleScales';
5
6
  export { createZoraColorScale, type CreateZoraColorScaleOptions, createZoraNeutralScale, createZoraPrimaryScale, } from './scales';
6
7
  export { ZORA_COLOR_SCALE_STEPS, type ZoraColorScale, type ZoraColorScaleStep } from './types';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/internal/color/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,sBAAsB,EAAE,KAAK,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/internal/color/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,oBAAoB,EACpB,KAAK,2BAA2B,EAChC,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,sBAAsB,EAAE,KAAK,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,SAAS,CAAC"}
@@ -2,6 +2,7 @@ export { computeZoraHarmony, } from './harmony';
2
2
  export { parseHexToOklch } from './oklch';
3
3
  export { resolveModePrimaryColor } from './primary';
4
4
  export { assignZoraHarmonyRoleHues, getZoraHueRoleAssignment, } from './roleHues';
5
+ export { createZoraRoleColorScales, getZoraRoleColorScale, ZORA_COLOR_SCALE_ROLE_ORDER, } from './roleScales';
5
6
  export { createZoraColorScale, createZoraNeutralScale, createZoraPrimaryScale, } from './scales';
6
7
  export { ZORA_COLOR_SCALE_STEPS } from './types';
7
8
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/internal/color/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,GAInB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EACL,yBAAyB,EACzB,wBAAwB,GAIzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,oBAAoB,EAEpB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,sBAAsB,EAAgD,MAAM,SAAS,CAAC","sourcesContent":["export {\n computeZoraHarmony,\n type ZoraComputedHarmony,\n type ZoraHarmonySlot,\n type ZoraHarmonySlotId,\n} from './harmony';\nexport { parseHexToOklch } from './oklch';\nexport { resolveModePrimaryColor } from './primary';\nexport {\n assignZoraHarmonyRoleHues,\n getZoraHueRoleAssignment,\n type ZoraComputedHueRoles,\n type ZoraHueRoleAssignment,\n type ZoraHueRoleId,\n} from './roleHues';\nexport {\n createZoraColorScale,\n type CreateZoraColorScaleOptions,\n createZoraNeutralScale,\n createZoraPrimaryScale,\n} from './scales';\nexport { ZORA_COLOR_SCALE_STEPS, type ZoraColorScale, type ZoraColorScaleStep } from './types';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/internal/color/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,GAInB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EACL,yBAAyB,EACzB,wBAAwB,GAIzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,GAI5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,oBAAoB,EAEpB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,sBAAsB,EAAgD,MAAM,SAAS,CAAC","sourcesContent":["export {\n computeZoraHarmony,\n type ZoraComputedHarmony,\n type ZoraHarmonySlot,\n type ZoraHarmonySlotId,\n} from './harmony';\nexport { parseHexToOklch } from './oklch';\nexport { resolveModePrimaryColor } from './primary';\nexport {\n assignZoraHarmonyRoleHues,\n getZoraHueRoleAssignment,\n type ZoraComputedHueRoles,\n type ZoraHueRoleAssignment,\n type ZoraHueRoleId,\n} from './roleHues';\nexport {\n createZoraRoleColorScales,\n getZoraRoleColorScale,\n ZORA_COLOR_SCALE_ROLE_ORDER,\n type ZoraColorScaleRoleId,\n type ZoraComputedRoleColorScales,\n type ZoraRoleColorScale,\n} from './roleScales';\nexport {\n createZoraColorScale,\n type CreateZoraColorScaleOptions,\n createZoraNeutralScale,\n createZoraPrimaryScale,\n} from './scales';\nexport { ZORA_COLOR_SCALE_STEPS, type ZoraColorScale, type ZoraColorScaleStep } from './types';\n"]}
@@ -0,0 +1,19 @@
1
+ import type { ZoraHexColor } from '../../theme/types';
2
+ import { type ZoraComputedHueRoles } from './roleHues';
3
+ import type { ZoraColorScale } from './types';
4
+ export type ZoraColorScaleRoleId = 'primary' | 'secondary' | 'accent' | 'highlight' | 'surfaceTint' | 'neutral';
5
+ export declare const ZORA_COLOR_SCALE_ROLE_ORDER: readonly ZoraColorScaleRoleId[];
6
+ export interface ZoraRoleColorScale {
7
+ role: ZoraColorScaleRoleId;
8
+ sourceHue?: number;
9
+ scale: ZoraColorScale;
10
+ }
11
+ export interface ZoraComputedRoleColorScales {
12
+ roles: readonly ZoraRoleColorScale[];
13
+ }
14
+ export declare function getZoraRoleColorScale(scales: ZoraComputedRoleColorScales, role: ZoraColorScaleRoleId): ZoraRoleColorScale;
15
+ export declare function createZoraRoleColorScales(options: {
16
+ hueRoles: ZoraComputedHueRoles;
17
+ seed: ZoraHexColor;
18
+ }): ZoraComputedRoleColorScales;
19
+ //# sourceMappingURL=roleScales.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleScales.d.ts","sourceRoot":"","sources":["../../../src/internal/color/roleScales.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,YAAY,CAAC;AAOpB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,oBAAoB,GAC5B,SAAS,GACT,WAAW,GACX,QAAQ,GACR,WAAW,GACX,aAAa,GACb,SAAS,CAAC;AAEd,eAAO,MAAM,2BAA2B,EAAE,SAAS,oBAAoB,EAOtE,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;CACtC;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,2BAA2B,EACnC,IAAI,EAAE,oBAAoB,GACzB,kBAAkB,CAMpB;AA0BD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE;IACjD,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,IAAI,EAAE,YAAY,CAAC;CACpB,GAAG,2BAA2B,CAgB9B"}
@@ -0,0 +1,51 @@
1
+ import { parseHexToOklch } from './oklch';
2
+ import { getZoraHueRoleAssignment, } from './roleHues';
3
+ import { createZoraHueScale, createZoraNeutralScale, } from './scales';
4
+ export const ZORA_COLOR_SCALE_ROLE_ORDER = [
5
+ 'primary',
6
+ 'secondary',
7
+ 'accent',
8
+ 'highlight',
9
+ 'surfaceTint',
10
+ 'neutral',
11
+ ];
12
+ export function getZoraRoleColorScale(scales, role) {
13
+ const found = scales.roles.find((entry) => entry.role === role);
14
+ if (!found) {
15
+ throw new Error(`[zora] Expected role color scales to include "${role}".`);
16
+ }
17
+ return found;
18
+ }
19
+ function resolveSeedChroma(seed) {
20
+ return parseHexToOklch(seed).c;
21
+ }
22
+ function createHueBackedRoleScale(options) {
23
+ const assignment = getZoraHueRoleAssignment(options.hueRoles, options.role);
24
+ const hueScaleRole = options.role;
25
+ const hueScaleOptions = {
26
+ hue: assignment.hue,
27
+ seedChroma: options.seedChroma,
28
+ role: hueScaleRole,
29
+ };
30
+ return {
31
+ role: options.role,
32
+ sourceHue: assignment.hue,
33
+ scale: createZoraHueScale(hueScaleOptions),
34
+ };
35
+ }
36
+ export function createZoraRoleColorScales(options) {
37
+ const seedChroma = resolveSeedChroma(options.seed);
38
+ const roles = [
39
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'primary' }),
40
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'secondary' }),
41
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'accent' }),
42
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'highlight' }),
43
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'surfaceTint' }),
44
+ {
45
+ role: 'neutral',
46
+ scale: createZoraNeutralScale(options.seed),
47
+ },
48
+ ];
49
+ return { roles };
50
+ }
51
+ //# sourceMappingURL=roleScales.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleScales.js","sourceRoot":"","sources":["../../../src/internal/color/roleScales.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EACL,wBAAwB,GAGzB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,kBAAkB,EAElB,sBAAsB,GAEvB,MAAM,UAAU,CAAC;AAWlB,MAAM,CAAC,MAAM,2BAA2B,GAAoC;IAC1E,SAAS;IACT,WAAW;IACX,QAAQ;IACR,WAAW;IACX,aAAa;IACb,SAAS;CACV,CAAC;AAYF,MAAM,UAAU,qBAAqB,CACnC,MAAmC,EACnC,IAA0B;IAE1B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAChE,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,iDAAiD,IAAI,IAAI,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAkB;IAC3C,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,wBAAwB,CAAC,OAIjC;IACC,MAAM,UAAU,GAAG,wBAAwB,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5E,MAAM,YAAY,GAAuB,OAAO,CAAC,IAAI,CAAC;IACtD,MAAM,eAAe,GAA8B;QACjD,GAAG,EAAE,UAAU,CAAC,GAAG;QACnB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,IAAI,EAAE,YAAY;KACnB,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,SAAS,EAAE,UAAU,CAAC,GAAG;QACzB,KAAK,EAAE,kBAAkB,CAAC,eAAe,CAAC;KAC3C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,OAGzC;IACC,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,KAAK,GAAyB;QAClC,wBAAwB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QACrF,wBAAwB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QACvF,wBAAwB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QACpF,wBAAwB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;QACvF,wBAAwB,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QACzF;YACE,IAAI,EAAE,SAAS;YACf,KAAK,EAAE,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC;SAC5C;KACF,CAAC;IAEF,OAAO,EAAE,KAAK,EAAE,CAAC;AACnB,CAAC","sourcesContent":["import type { ZoraHexColor } from '../../theme/types';\nimport { parseHexToOklch } from './oklch';\nimport {\n getZoraHueRoleAssignment,\n type ZoraComputedHueRoles,\n type ZoraHueRoleId,\n} from './roleHues';\nimport {\n createZoraHueScale,\n type CreateZoraHueScaleOptions,\n createZoraNeutralScale,\n type ZoraHueScaleRoleId,\n} from './scales';\nimport type { ZoraColorScale } from './types';\n\nexport type ZoraColorScaleRoleId =\n | 'primary'\n | 'secondary'\n | 'accent'\n | 'highlight'\n | 'surfaceTint'\n | 'neutral';\n\nexport const ZORA_COLOR_SCALE_ROLE_ORDER: readonly ZoraColorScaleRoleId[] = [\n 'primary',\n 'secondary',\n 'accent',\n 'highlight',\n 'surfaceTint',\n 'neutral',\n];\n\nexport interface ZoraRoleColorScale {\n role: ZoraColorScaleRoleId;\n sourceHue?: number;\n scale: ZoraColorScale;\n}\n\nexport interface ZoraComputedRoleColorScales {\n roles: readonly ZoraRoleColorScale[];\n}\n\nexport function getZoraRoleColorScale(\n scales: ZoraComputedRoleColorScales,\n role: ZoraColorScaleRoleId,\n): ZoraRoleColorScale {\n const found = scales.roles.find((entry) => entry.role === role);\n if (!found) {\n throw new Error(`[zora] Expected role color scales to include \"${role}\".`);\n }\n return found;\n}\n\nfunction resolveSeedChroma(seed: ZoraHexColor): number {\n return parseHexToOklch(seed).c;\n}\n\nfunction createHueBackedRoleScale(options: {\n hueRoles: ZoraComputedHueRoles;\n seedChroma: number;\n role: ZoraHueRoleId;\n}): ZoraRoleColorScale {\n const assignment = getZoraHueRoleAssignment(options.hueRoles, options.role);\n const hueScaleRole: ZoraHueScaleRoleId = options.role;\n const hueScaleOptions: CreateZoraHueScaleOptions = {\n hue: assignment.hue,\n seedChroma: options.seedChroma,\n role: hueScaleRole,\n };\n\n return {\n role: options.role,\n sourceHue: assignment.hue,\n scale: createZoraHueScale(hueScaleOptions),\n };\n}\n\nexport function createZoraRoleColorScales(options: {\n hueRoles: ZoraComputedHueRoles;\n seed: ZoraHexColor;\n}): ZoraComputedRoleColorScales {\n const seedChroma = resolveSeedChroma(options.seed);\n\n const roles: ZoraRoleColorScale[] = [\n createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'primary' }),\n createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'secondary' }),\n createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'accent' }),\n createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'highlight' }),\n createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'surfaceTint' }),\n {\n role: 'neutral',\n scale: createZoraNeutralScale(options.seed),\n },\n ];\n\n return { roles };\n}\n"]}
@@ -4,7 +4,14 @@ export interface CreateZoraColorScaleOptions {
4
4
  seed: ZoraHexColor;
5
5
  role?: 'primary' | 'neutral';
6
6
  }
7
+ export type ZoraHueScaleRoleId = 'primary' | 'secondary' | 'accent' | 'highlight' | 'surfaceTint';
8
+ export interface CreateZoraHueScaleOptions {
9
+ hue: number;
10
+ seedChroma: number;
11
+ role: ZoraHueScaleRoleId;
12
+ }
7
13
  export declare function createZoraColorScale(options: CreateZoraColorScaleOptions): ZoraColorScale;
8
14
  export declare function createZoraPrimaryScale(seed: ZoraHexColor): ZoraColorScale;
9
15
  export declare function createZoraNeutralScale(seed?: ZoraHexColor): ZoraColorScale;
16
+ export declare function createZoraHueScale(options: CreateZoraHueScaleOptions): ZoraColorScale;
10
17
  //# sourceMappingURL=scales.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scales.d.ts","sourceRoot":"","sources":["../../../src/internal/color/scales.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,KAAK,cAAc,EAA2B,MAAM,SAAS,CAAC;AAEvE,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B;AA4HD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,cAAc,CAKzF;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,cAAc,CAEzE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,YAAwB,GAAG,cAAc,CAErF"}
1
+ {"version":3,"file":"scales.d.ts","sourceRoot":"","sources":["../../../src/internal/color/scales.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,OAAO,EAAE,KAAK,cAAc,EAA2B,MAAM,SAAS,CAAC;AAEvE,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAAC;AAElG,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,kBAAkB,CAAC;CAC1B;AA4ID,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,cAAc,CAKzF;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,cAAc,CAEzE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,YAAwB,GAAG,cAAc,CAErF;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,cAAc,CAMrF"}
@@ -43,6 +43,13 @@ const MAX_PRIMARY_SCALE_CHROMA = 0.2;
43
43
  const MIN_PRIMARY_SCALE_CHROMA = 0.04;
44
44
  const NEUTRAL_CHROMA = 0.012;
45
45
  const DEFAULT_NEUTRAL_HUE_DEGREES = 260;
46
+ const ROLE_CHROMA_FACTOR = {
47
+ primary: 1,
48
+ secondary: 0.72,
49
+ accent: 0.85,
50
+ highlight: 1,
51
+ surfaceTint: 0.18,
52
+ };
46
53
  function clampNumber(value, min, max) {
47
54
  return Math.max(min, Math.min(value, max));
48
55
  }
@@ -54,6 +61,10 @@ function resolvePrimaryScaleChroma(seedChroma, step) {
54
61
  const shouldEnforceMin = step >= 300 && step <= 700 && seedChroma >= MIN_PRIMARY_SCALE_CHROMA;
55
62
  return shouldEnforceMin ? Math.max(bounded, MIN_PRIMARY_SCALE_CHROMA) : bounded;
56
63
  }
64
+ function resolveHueScaleChroma(options, step) {
65
+ const factor = ROLE_CHROMA_FACTOR[options.role];
66
+ return resolvePrimaryScaleChroma(options.seedChroma * factor, step);
67
+ }
57
68
  function createScaleEntries(options) {
58
69
  const seed = parseHexToOklch(options.seed);
59
70
  if (options.role === 'neutral') {
@@ -107,4 +118,11 @@ export function createZoraPrimaryScale(seed) {
107
118
  export function createZoraNeutralScale(seed = '#94a3b8') {
108
119
  return createZoraColorScale({ seed, role: 'neutral' });
109
120
  }
121
+ export function createZoraHueScale(options) {
122
+ return createScaleFromRamp({
123
+ hue: options.hue,
124
+ chromaByStep: (step) => resolveHueScaleChroma(options, step),
125
+ lightnessByStep: PRIMARY_LIGHTNESS_BY_STEP,
126
+ });
127
+ }
110
128
  //# sourceMappingURL=scales.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scales.js","sourceRoot":"","sources":["../../../src/internal/color/scales.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ZoraHexColor } from '../../theme/types';\nimport { clampOklchToGamut, formatOklchAsHex, parseHexToOklch } from './oklch';\nimport { type ZoraColorScale, type ZoraColorScaleStep } from './types';\n\nexport interface CreateZoraColorScaleOptions {\n seed: ZoraHexColor;\n role?: 'primary' | 'neutral';\n}\n\nconst PRIMARY_LIGHTNESS_BY_STEP: Record<ZoraColorScaleStep, number> = {\n 50: 0.97,\n 100: 0.93,\n 200: 0.86,\n 300: 0.78,\n 400: 0.68,\n 500: 0.58,\n 600: 0.5,\n 700: 0.42,\n 800: 0.34,\n 900: 0.27,\n 950: 0.2,\n};\n\nconst NEUTRAL_LIGHTNESS_BY_STEP: Record<ZoraColorScaleStep, number> = {\n 50: 0.98,\n 100: 0.95,\n 200: 0.89,\n 300: 0.8,\n 400: 0.68,\n 500: 0.55,\n 600: 0.44,\n 700: 0.34,\n 800: 0.25,\n 900: 0.18,\n 950: 0.12,\n};\n\nconst PRIMARY_CHROMA_MULTIPLIER_BY_STEP: Record<ZoraColorScaleStep, number> = {\n 50: 0.2,\n 100: 0.3,\n 200: 0.45,\n 300: 0.7,\n 400: 0.95,\n 500: 1,\n 600: 0.95,\n 700: 0.85,\n 800: 0.65,\n 900: 0.45,\n 950: 0.3,\n};\n\nconst MAX_PRIMARY_SCALE_CHROMA = 0.2;\nconst MIN_PRIMARY_SCALE_CHROMA = 0.04;\nconst NEUTRAL_CHROMA = 0.012;\nconst DEFAULT_NEUTRAL_HUE_DEGREES = 260;\n\nfunction clampNumber(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(value, max));\n}\n\nfunction resolvePrimaryScaleChroma(seedChroma: number, step: ZoraColorScaleStep): number {\n const cappedSeedChroma = clampNumber(seedChroma, 0, MAX_PRIMARY_SCALE_CHROMA);\n const multiplier = PRIMARY_CHROMA_MULTIPLIER_BY_STEP[step];\n const scaled = cappedSeedChroma * multiplier;\n\n const bounded = clampNumber(scaled, 0, MAX_PRIMARY_SCALE_CHROMA);\n const shouldEnforceMin = step >= 300 && step <= 700 && seedChroma >= MIN_PRIMARY_SCALE_CHROMA;\n\n return shouldEnforceMin ? Math.max(bounded, MIN_PRIMARY_SCALE_CHROMA) : bounded;\n}\n\nfunction createScaleEntries(options: CreateZoraColorScaleOptions): ZoraColorScale {\n const seed = parseHexToOklch(options.seed);\n\n if (options.role === 'neutral') {\n const hue = typeof seed.h === 'number' ? seed.h : DEFAULT_NEUTRAL_HUE_DEGREES;\n\n return createScaleFromRamp({\n hue,\n chroma: NEUTRAL_CHROMA,\n lightnessByStep: NEUTRAL_LIGHTNESS_BY_STEP,\n });\n }\n\n return createScaleFromRamp({\n hue: seed.h,\n chromaByStep: (step) => resolvePrimaryScaleChroma(seed.c, step),\n lightnessByStep: PRIMARY_LIGHTNESS_BY_STEP,\n });\n}\n\ninterface CreateScaleFromRampOptions {\n hue: number;\n chroma?: number;\n chromaByStep?: (step: ZoraColorScaleStep) => number;\n lightnessByStep: Record<ZoraColorScaleStep, number>;\n}\n\nfunction createScaleColor(\n options: CreateScaleFromRampOptions,\n step: ZoraColorScaleStep,\n): ZoraHexColor {\n const lightness = options.lightnessByStep[step];\n const chroma =\n typeof options.chromaByStep === 'function' ? options.chromaByStep(step) : (options.chroma ?? 0);\n\n const clamped = clampOklchToGamut({\n l: clampNumber(lightness, 0, 1),\n c: clampNumber(chroma, 0, 1),\n h: options.hue,\n });\n\n return formatOklchAsHex(clamped);\n}\n\nfunction createScaleFromRamp(options: CreateScaleFromRampOptions): ZoraColorScale {\n return {\n 50: createScaleColor(options, 50),\n 100: createScaleColor(options, 100),\n 200: createScaleColor(options, 200),\n 300: createScaleColor(options, 300),\n 400: createScaleColor(options, 400),\n 500: createScaleColor(options, 500),\n 600: createScaleColor(options, 600),\n 700: createScaleColor(options, 700),\n 800: createScaleColor(options, 800),\n 900: createScaleColor(options, 900),\n 950: createScaleColor(options, 950),\n };\n}\n\nexport function createZoraColorScale(options: CreateZoraColorScaleOptions): ZoraColorScale {\n return createScaleEntries({\n seed: options.seed,\n role: options.role,\n });\n}\n\nexport function createZoraPrimaryScale(seed: ZoraHexColor): ZoraColorScale {\n return createZoraColorScale({ seed, role: 'primary' });\n}\n\nexport function createZoraNeutralScale(seed: ZoraHexColor = '#94a3b8'): ZoraColorScale {\n return createZoraColorScale({ seed, role: 'neutral' });\n}\n"]}
1
+ {"version":3,"file":"scales.js","sourceRoot":"","sources":["../../../src/internal/color/scales.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ZoraHexColor } from '../../theme/types';\nimport { clampOklchToGamut, formatOklchAsHex, parseHexToOklch } from './oklch';\nimport { type ZoraColorScale, type ZoraColorScaleStep } from './types';\n\nexport interface CreateZoraColorScaleOptions {\n seed: ZoraHexColor;\n role?: 'primary' | 'neutral';\n}\n\nexport type ZoraHueScaleRoleId = 'primary' | 'secondary' | 'accent' | 'highlight' | 'surfaceTint';\n\nexport interface CreateZoraHueScaleOptions {\n hue: number;\n seedChroma: number;\n role: ZoraHueScaleRoleId;\n}\n\nconst PRIMARY_LIGHTNESS_BY_STEP: Record<ZoraColorScaleStep, number> = {\n 50: 0.97,\n 100: 0.93,\n 200: 0.86,\n 300: 0.78,\n 400: 0.68,\n 500: 0.58,\n 600: 0.5,\n 700: 0.42,\n 800: 0.34,\n 900: 0.27,\n 950: 0.2,\n};\n\nconst NEUTRAL_LIGHTNESS_BY_STEP: Record<ZoraColorScaleStep, number> = {\n 50: 0.98,\n 100: 0.95,\n 200: 0.89,\n 300: 0.8,\n 400: 0.68,\n 500: 0.55,\n 600: 0.44,\n 700: 0.34,\n 800: 0.25,\n 900: 0.18,\n 950: 0.12,\n};\n\nconst PRIMARY_CHROMA_MULTIPLIER_BY_STEP: Record<ZoraColorScaleStep, number> = {\n 50: 0.2,\n 100: 0.3,\n 200: 0.45,\n 300: 0.7,\n 400: 0.95,\n 500: 1,\n 600: 0.95,\n 700: 0.85,\n 800: 0.65,\n 900: 0.45,\n 950: 0.3,\n};\n\nconst MAX_PRIMARY_SCALE_CHROMA = 0.2;\nconst MIN_PRIMARY_SCALE_CHROMA = 0.04;\nconst NEUTRAL_CHROMA = 0.012;\nconst DEFAULT_NEUTRAL_HUE_DEGREES = 260;\n\nconst ROLE_CHROMA_FACTOR = {\n primary: 1,\n secondary: 0.72,\n accent: 0.85,\n highlight: 1,\n surfaceTint: 0.18,\n} satisfies Record<ZoraHueScaleRoleId, number>;\n\nfunction clampNumber(value: number, min: number, max: number): number {\n return Math.max(min, Math.min(value, max));\n}\n\nfunction resolvePrimaryScaleChroma(seedChroma: number, step: ZoraColorScaleStep): number {\n const cappedSeedChroma = clampNumber(seedChroma, 0, MAX_PRIMARY_SCALE_CHROMA);\n const multiplier = PRIMARY_CHROMA_MULTIPLIER_BY_STEP[step];\n const scaled = cappedSeedChroma * multiplier;\n\n const bounded = clampNumber(scaled, 0, MAX_PRIMARY_SCALE_CHROMA);\n const shouldEnforceMin = step >= 300 && step <= 700 && seedChroma >= MIN_PRIMARY_SCALE_CHROMA;\n\n return shouldEnforceMin ? Math.max(bounded, MIN_PRIMARY_SCALE_CHROMA) : bounded;\n}\n\nfunction resolveHueScaleChroma(\n options: CreateZoraHueScaleOptions,\n step: ZoraColorScaleStep,\n): number {\n const factor = ROLE_CHROMA_FACTOR[options.role];\n return resolvePrimaryScaleChroma(options.seedChroma * factor, step);\n}\n\nfunction createScaleEntries(options: CreateZoraColorScaleOptions): ZoraColorScale {\n const seed = parseHexToOklch(options.seed);\n\n if (options.role === 'neutral') {\n const hue = typeof seed.h === 'number' ? seed.h : DEFAULT_NEUTRAL_HUE_DEGREES;\n\n return createScaleFromRamp({\n hue,\n chroma: NEUTRAL_CHROMA,\n lightnessByStep: NEUTRAL_LIGHTNESS_BY_STEP,\n });\n }\n\n return createScaleFromRamp({\n hue: seed.h,\n chromaByStep: (step) => resolvePrimaryScaleChroma(seed.c, step),\n lightnessByStep: PRIMARY_LIGHTNESS_BY_STEP,\n });\n}\n\ninterface CreateScaleFromRampOptions {\n hue: number;\n chroma?: number;\n chromaByStep?: (step: ZoraColorScaleStep) => number;\n lightnessByStep: Record<ZoraColorScaleStep, number>;\n}\n\nfunction createScaleColor(\n options: CreateScaleFromRampOptions,\n step: ZoraColorScaleStep,\n): ZoraHexColor {\n const lightness = options.lightnessByStep[step];\n const chroma =\n typeof options.chromaByStep === 'function' ? options.chromaByStep(step) : (options.chroma ?? 0);\n\n const clamped = clampOklchToGamut({\n l: clampNumber(lightness, 0, 1),\n c: clampNumber(chroma, 0, 1),\n h: options.hue,\n });\n\n return formatOklchAsHex(clamped);\n}\n\nfunction createScaleFromRamp(options: CreateScaleFromRampOptions): ZoraColorScale {\n return {\n 50: createScaleColor(options, 50),\n 100: createScaleColor(options, 100),\n 200: createScaleColor(options, 200),\n 300: createScaleColor(options, 300),\n 400: createScaleColor(options, 400),\n 500: createScaleColor(options, 500),\n 600: createScaleColor(options, 600),\n 700: createScaleColor(options, 700),\n 800: createScaleColor(options, 800),\n 900: createScaleColor(options, 900),\n 950: createScaleColor(options, 950),\n };\n}\n\nexport function createZoraColorScale(options: CreateZoraColorScaleOptions): ZoraColorScale {\n return createScaleEntries({\n seed: options.seed,\n role: options.role,\n });\n}\n\nexport function createZoraPrimaryScale(seed: ZoraHexColor): ZoraColorScale {\n return createZoraColorScale({ seed, role: 'primary' });\n}\n\nexport function createZoraNeutralScale(seed: ZoraHexColor = '#94a3b8'): ZoraColorScale {\n return createZoraColorScale({ seed, role: 'neutral' });\n}\n\nexport function createZoraHueScale(options: CreateZoraHueScaleOptions): ZoraColorScale {\n return createScaleFromRamp({\n hue: options.hue,\n chromaByStep: (step) => resolveHueScaleChroma(options, step),\n lightnessByStep: PRIMARY_LIGHTNESS_BY_STEP,\n });\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ankhorage/zora",
3
3
  "type": "module",
4
- "version": "0.12.2",
4
+ "version": "0.12.3",
5
5
  "description": "Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.",
6
6
  "homepage": "https://github.com/ankhorage/zora#readme",
7
7
  "bugs": {
@@ -13,6 +13,14 @@ export {
13
13
  type ZoraHueRoleAssignment,
14
14
  type ZoraHueRoleId,
15
15
  } from './roleHues';
16
+ export {
17
+ createZoraRoleColorScales,
18
+ getZoraRoleColorScale,
19
+ ZORA_COLOR_SCALE_ROLE_ORDER,
20
+ type ZoraColorScaleRoleId,
21
+ type ZoraComputedRoleColorScales,
22
+ type ZoraRoleColorScale,
23
+ } from './roleScales';
16
24
  export {
17
25
  createZoraColorScale,
18
26
  type CreateZoraColorScaleOptions,
@@ -25,7 +25,7 @@ const ROLE_ORDER: readonly ZoraHueRoleId[] = [
25
25
 
26
26
  function expectEveryRoleOnce(roles: ReturnType<typeof assignZoraHarmonyRoleHues>) {
27
27
  expect(roles.assignments).toHaveLength(ROLE_ORDER.length);
28
- expect(roles.assignments.map((assignment) => assignment.role)).toEqual(ROLE_ORDER);
28
+ expect(roles.assignments.map((assignment) => assignment.role)).toEqual([...ROLE_ORDER]);
29
29
  }
30
30
 
31
31
  describe('assignZoraHarmonyRoleHues', () => {
@@ -0,0 +1,247 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import type { ZoraHexColor } from '../../theme/types';
4
+ import {
5
+ assignZoraHarmonyRoleHues,
6
+ computeZoraHarmony,
7
+ createZoraRoleColorScales,
8
+ getZoraRoleColorScale,
9
+ parseHexToOklch,
10
+ ZORA_COLOR_SCALE_ROLE_ORDER,
11
+ ZORA_COLOR_SCALE_STEPS,
12
+ type ZoraColorScale,
13
+ type ZoraColorScaleRoleId,
14
+ type ZoraColorScaleStep,
15
+ type ZoraComputedHueRoles,
16
+ type ZoraComputedRoleColorScales,
17
+ type ZoraRoleColorScale,
18
+ } from './index';
19
+
20
+ function isSixDigitLowercaseHexColor(value: string): value is ZoraHexColor {
21
+ return /^#[0-9a-f]{6}$/.test(value);
22
+ }
23
+
24
+ function hueDeltaDegrees(a: number, b: number): number {
25
+ const raw = Math.abs(a - b) % 360;
26
+ return Math.min(raw, 360 - raw);
27
+ }
28
+
29
+ function getStepValues(scale: ZoraColorScale): { step: ZoraColorScaleStep; hex: ZoraHexColor }[] {
30
+ return ZORA_COLOR_SCALE_STEPS.map((step) => ({ step, hex: scale[step] }));
31
+ }
32
+
33
+ function assertScaleKeys(scale: ZoraColorScale) {
34
+ const keys = Object.keys(scale)
35
+ .map((key) => Number(key))
36
+ .sort((a, b) => a - b);
37
+
38
+ expect(keys).toEqual([...ZORA_COLOR_SCALE_STEPS]);
39
+ }
40
+
41
+ function assertNoPureBlackOrWhite(scale: ZoraColorScale) {
42
+ const values = getStepValues(scale).map(({ hex }) => hex);
43
+ expect(values).not.toContain('#000000');
44
+ expect(values).not.toContain('#ffffff');
45
+ }
46
+
47
+ function buildHueRoles(assignments: ZoraComputedHueRoles['assignments']): ZoraComputedHueRoles {
48
+ return {
49
+ harmony: 'tetradic',
50
+ assignments,
51
+ };
52
+ }
53
+
54
+ function requireHueBackedSourceHue(role: ZoraRoleColorScale): number {
55
+ if (typeof role.sourceHue !== 'number') {
56
+ throw new Error(`[zora] Expected "${role.role}" role scale to include a sourceHue.`);
57
+ }
58
+ return role.sourceHue;
59
+ }
60
+
61
+ function expectRoleOrder(
62
+ scales: ZoraComputedRoleColorScales,
63
+ expected: readonly ZoraColorScaleRoleId[],
64
+ ) {
65
+ expect(scales.roles.map((entry) => entry.role)).toEqual([...expected]);
66
+ }
67
+
68
+ describe('createZoraRoleColorScales', () => {
69
+ test('returns all roles exactly once in deterministic order', () => {
70
+ const hueRoles = buildHueRoles([
71
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
72
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
73
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
74
+ { role: 'highlight', hue: 20, sourceSlotId: 'c' },
75
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
76
+ ]);
77
+
78
+ const scales: ZoraComputedRoleColorScales = createZoraRoleColorScales({
79
+ hueRoles,
80
+ seed: '#0f766e',
81
+ });
82
+
83
+ const expectedOrder: readonly ZoraColorScaleRoleId[] = ZORA_COLOR_SCALE_ROLE_ORDER;
84
+ expect(scales.roles).toHaveLength(expectedOrder.length);
85
+
86
+ expectRoleOrder(scales, expectedOrder);
87
+ expect(new Set(scales.roles.map((entry) => entry.role)).size).toBe(
88
+ ZORA_COLOR_SCALE_ROLE_ORDER.length,
89
+ );
90
+ });
91
+
92
+ test('every scale contains exactly all 50–950 keys and valid lowercase 6-digit hex values', () => {
93
+ const hueRoles = buildHueRoles([
94
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
95
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
96
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
97
+ { role: 'highlight', hue: 20, sourceSlotId: 'c' },
98
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
99
+ ]);
100
+
101
+ const scales = createZoraRoleColorScales({ hueRoles, seed: '#0f766e' });
102
+
103
+ for (const role of scales.roles) {
104
+ assertScaleKeys(role.scale);
105
+ for (const { hex } of getStepValues(role.scale)) {
106
+ expect(isSixDigitLowercaseHexColor(hex)).toBe(true);
107
+ }
108
+ }
109
+ });
110
+
111
+ test('output is deterministic for the same input', () => {
112
+ const hueRoles = buildHueRoles([
113
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
114
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
115
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
116
+ { role: 'highlight', hue: 20, sourceSlotId: 'c' },
117
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
118
+ ]);
119
+
120
+ const seed: ZoraHexColor = '#0f766e';
121
+
122
+ expect(createZoraRoleColorScales({ hueRoles, seed })).toEqual(
123
+ createZoraRoleColorScales({ hueRoles, seed }),
124
+ );
125
+ });
126
+
127
+ test('role hue is preserved approximately for mid steps', () => {
128
+ const hueRoles = buildHueRoles([
129
+ { role: 'primary', hue: 115, sourceSlotId: 'base' },
130
+ { role: 'secondary', hue: 210, sourceSlotId: 'a' },
131
+ { role: 'accent', hue: 300, sourceSlotId: 'b' },
132
+ { role: 'highlight', hue: 25, sourceSlotId: 'c' },
133
+ { role: 'surfaceTint', hue: 165, sourceSlotId: 'a' },
134
+ ]);
135
+
136
+ const scales = createZoraRoleColorScales({ hueRoles, seed: '#0f766e' });
137
+
138
+ const midSteps: ZoraColorScaleStep[] = [400, 500, 600, 700];
139
+ for (const roleId of ['primary', 'secondary', 'accent', 'highlight', 'surfaceTint'] as const) {
140
+ const role = getZoraRoleColorScale(scales, roleId);
141
+ const expectedHue = requireHueBackedSourceHue(role);
142
+
143
+ for (const step of midSteps) {
144
+ const oklch = parseHexToOklch(role.scale[step]);
145
+ expect(hueDeltaDegrees(expectedHue, oklch.h)).toBeLessThan(25);
146
+ }
147
+ }
148
+ });
149
+
150
+ test('surfaceTint scale has lower chroma than primary/accent/highlight for mid steps', () => {
151
+ const hueRoles = buildHueRoles([
152
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
153
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
154
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
155
+ { role: 'highlight', hue: 20, sourceSlotId: 'c' },
156
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
157
+ ]);
158
+
159
+ const scales = createZoraRoleColorScales({ hueRoles, seed: '#0f766e' });
160
+
161
+ const surfaceTint = getZoraRoleColorScale(scales, 'surfaceTint').scale;
162
+ const primary = getZoraRoleColorScale(scales, 'primary').scale;
163
+ const accent = getZoraRoleColorScale(scales, 'accent').scale;
164
+ const highlight = getZoraRoleColorScale(scales, 'highlight').scale;
165
+
166
+ const step: ZoraColorScaleStep = 500;
167
+ const surfaceTintChroma = parseHexToOklch(surfaceTint[step]).c;
168
+
169
+ expect(surfaceTintChroma).toBeLessThan(parseHexToOklch(primary[step]).c);
170
+ expect(surfaceTintChroma).toBeLessThan(parseHexToOklch(accent[step]).c);
171
+ expect(surfaceTintChroma).toBeLessThan(parseHexToOklch(highlight[step]).c);
172
+ });
173
+
174
+ test('neutral scale has low chroma', () => {
175
+ const hueRoles = buildHueRoles([
176
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
177
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
178
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
179
+ { role: 'highlight', hue: 20, sourceSlotId: 'c' },
180
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
181
+ ]);
182
+
183
+ const scales = createZoraRoleColorScales({ hueRoles, seed: '#0f766e' });
184
+ const neutral = getZoraRoleColorScale(scales, 'neutral').scale;
185
+
186
+ for (const { hex } of getStepValues(neutral)) {
187
+ expect(parseHexToOklch(hex).c).toBeLessThanOrEqual(0.03);
188
+ }
189
+ });
190
+
191
+ test('missing required hue role throws a clear error', () => {
192
+ const hueRoles = buildHueRoles([
193
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
194
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
195
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
196
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
197
+ ]);
198
+
199
+ expect(() => createZoraRoleColorScales({ hueRoles, seed: '#0f766e' })).toThrow('highlight');
200
+ });
201
+
202
+ test('no role scale contains pure black/white by default', () => {
203
+ const hueRoles = buildHueRoles([
204
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
205
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
206
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
207
+ { role: 'highlight', hue: 20, sourceSlotId: 'c' },
208
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
209
+ ]);
210
+
211
+ const scales = createZoraRoleColorScales({ hueRoles, seed: '#0f766e' });
212
+
213
+ for (const role of scales.roles) {
214
+ assertNoPureBlackOrWhite(role.scale);
215
+ }
216
+ });
217
+
218
+ test('does not mutate input hueRoles', () => {
219
+ const assignments: ZoraComputedHueRoles['assignments'] = [
220
+ { role: 'primary', hue: 120, sourceSlotId: 'base' },
221
+ { role: 'secondary', hue: 200, sourceSlotId: 'a' },
222
+ { role: 'accent', hue: 280, sourceSlotId: 'b' },
223
+ { role: 'highlight', hue: 20, sourceSlotId: 'c' },
224
+ { role: 'surfaceTint', hue: 160, sourceSlotId: 'a' },
225
+ ];
226
+
227
+ const hueRoles: ZoraComputedHueRoles = buildHueRoles(assignments);
228
+ const snapshot = JSON.stringify(hueRoles);
229
+
230
+ createZoraRoleColorScales({ hueRoles, seed: '#0f766e' });
231
+
232
+ expect(JSON.stringify(hueRoles)).toBe(snapshot);
233
+ });
234
+
235
+ test('integration: harmony -> role hues -> role scales pipeline connects', () => {
236
+ const seed: ZoraHexColor = '#0f766e';
237
+ const harmony = computeZoraHarmony(seed, 'tetradic');
238
+ const hueRoles = assignZoraHarmonyRoleHues(harmony);
239
+
240
+ const scales = createZoraRoleColorScales({ hueRoles, seed });
241
+
242
+ expect(scales.roles.map((entry) => entry.role)).toEqual([...ZORA_COLOR_SCALE_ROLE_ORDER]);
243
+ for (const role of scales.roles) {
244
+ assertScaleKeys(role.scale);
245
+ }
246
+ });
247
+ });
@@ -0,0 +1,97 @@
1
+ import type { ZoraHexColor } from '../../theme/types';
2
+ import { parseHexToOklch } from './oklch';
3
+ import {
4
+ getZoraHueRoleAssignment,
5
+ type ZoraComputedHueRoles,
6
+ type ZoraHueRoleId,
7
+ } from './roleHues';
8
+ import {
9
+ createZoraHueScale,
10
+ type CreateZoraHueScaleOptions,
11
+ createZoraNeutralScale,
12
+ type ZoraHueScaleRoleId,
13
+ } from './scales';
14
+ import type { ZoraColorScale } from './types';
15
+
16
+ export type ZoraColorScaleRoleId =
17
+ | 'primary'
18
+ | 'secondary'
19
+ | 'accent'
20
+ | 'highlight'
21
+ | 'surfaceTint'
22
+ | 'neutral';
23
+
24
+ export const ZORA_COLOR_SCALE_ROLE_ORDER: readonly ZoraColorScaleRoleId[] = [
25
+ 'primary',
26
+ 'secondary',
27
+ 'accent',
28
+ 'highlight',
29
+ 'surfaceTint',
30
+ 'neutral',
31
+ ];
32
+
33
+ export interface ZoraRoleColorScale {
34
+ role: ZoraColorScaleRoleId;
35
+ sourceHue?: number;
36
+ scale: ZoraColorScale;
37
+ }
38
+
39
+ export interface ZoraComputedRoleColorScales {
40
+ roles: readonly ZoraRoleColorScale[];
41
+ }
42
+
43
+ export function getZoraRoleColorScale(
44
+ scales: ZoraComputedRoleColorScales,
45
+ role: ZoraColorScaleRoleId,
46
+ ): ZoraRoleColorScale {
47
+ const found = scales.roles.find((entry) => entry.role === role);
48
+ if (!found) {
49
+ throw new Error(`[zora] Expected role color scales to include "${role}".`);
50
+ }
51
+ return found;
52
+ }
53
+
54
+ function resolveSeedChroma(seed: ZoraHexColor): number {
55
+ return parseHexToOklch(seed).c;
56
+ }
57
+
58
+ function createHueBackedRoleScale(options: {
59
+ hueRoles: ZoraComputedHueRoles;
60
+ seedChroma: number;
61
+ role: ZoraHueRoleId;
62
+ }): ZoraRoleColorScale {
63
+ const assignment = getZoraHueRoleAssignment(options.hueRoles, options.role);
64
+ const hueScaleRole: ZoraHueScaleRoleId = options.role;
65
+ const hueScaleOptions: CreateZoraHueScaleOptions = {
66
+ hue: assignment.hue,
67
+ seedChroma: options.seedChroma,
68
+ role: hueScaleRole,
69
+ };
70
+
71
+ return {
72
+ role: options.role,
73
+ sourceHue: assignment.hue,
74
+ scale: createZoraHueScale(hueScaleOptions),
75
+ };
76
+ }
77
+
78
+ export function createZoraRoleColorScales(options: {
79
+ hueRoles: ZoraComputedHueRoles;
80
+ seed: ZoraHexColor;
81
+ }): ZoraComputedRoleColorScales {
82
+ const seedChroma = resolveSeedChroma(options.seed);
83
+
84
+ const roles: ZoraRoleColorScale[] = [
85
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'primary' }),
86
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'secondary' }),
87
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'accent' }),
88
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'highlight' }),
89
+ createHueBackedRoleScale({ hueRoles: options.hueRoles, seedChroma, role: 'surfaceTint' }),
90
+ {
91
+ role: 'neutral',
92
+ scale: createZoraNeutralScale(options.seed),
93
+ },
94
+ ];
95
+
96
+ return { roles };
97
+ }
@@ -7,6 +7,14 @@ export interface CreateZoraColorScaleOptions {
7
7
  role?: 'primary' | 'neutral';
8
8
  }
9
9
 
10
+ export type ZoraHueScaleRoleId = 'primary' | 'secondary' | 'accent' | 'highlight' | 'surfaceTint';
11
+
12
+ export interface CreateZoraHueScaleOptions {
13
+ hue: number;
14
+ seedChroma: number;
15
+ role: ZoraHueScaleRoleId;
16
+ }
17
+
10
18
  const PRIMARY_LIGHTNESS_BY_STEP: Record<ZoraColorScaleStep, number> = {
11
19
  50: 0.97,
12
20
  100: 0.93,
@@ -54,6 +62,14 @@ const MIN_PRIMARY_SCALE_CHROMA = 0.04;
54
62
  const NEUTRAL_CHROMA = 0.012;
55
63
  const DEFAULT_NEUTRAL_HUE_DEGREES = 260;
56
64
 
65
+ const ROLE_CHROMA_FACTOR = {
66
+ primary: 1,
67
+ secondary: 0.72,
68
+ accent: 0.85,
69
+ highlight: 1,
70
+ surfaceTint: 0.18,
71
+ } satisfies Record<ZoraHueScaleRoleId, number>;
72
+
57
73
  function clampNumber(value: number, min: number, max: number): number {
58
74
  return Math.max(min, Math.min(value, max));
59
75
  }
@@ -69,6 +85,14 @@ function resolvePrimaryScaleChroma(seedChroma: number, step: ZoraColorScaleStep)
69
85
  return shouldEnforceMin ? Math.max(bounded, MIN_PRIMARY_SCALE_CHROMA) : bounded;
70
86
  }
71
87
 
88
+ function resolveHueScaleChroma(
89
+ options: CreateZoraHueScaleOptions,
90
+ step: ZoraColorScaleStep,
91
+ ): number {
92
+ const factor = ROLE_CHROMA_FACTOR[options.role];
93
+ return resolvePrimaryScaleChroma(options.seedChroma * factor, step);
94
+ }
95
+
72
96
  function createScaleEntries(options: CreateZoraColorScaleOptions): ZoraColorScale {
73
97
  const seed = parseHexToOklch(options.seed);
74
98
 
@@ -143,3 +167,11 @@ export function createZoraPrimaryScale(seed: ZoraHexColor): ZoraColorScale {
143
167
  export function createZoraNeutralScale(seed: ZoraHexColor = '#94a3b8'): ZoraColorScale {
144
168
  return createZoraColorScale({ seed, role: 'neutral' });
145
169
  }
170
+
171
+ export function createZoraHueScale(options: CreateZoraHueScaleOptions): ZoraColorScale {
172
+ return createScaleFromRamp({
173
+ hue: options.hue,
174
+ chromaByStep: (step) => resolveHueScaleChroma(options, step),
175
+ lightnessByStep: PRIMARY_LIGHTNESS_BY_STEP,
176
+ });
177
+ }