@ankhorage/zora 0.12.1 → 0.12.2

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.2
4
+
5
+ ### Patch Changes
6
+
7
+ - da31347: Adds internal hue-role assignment for future ZORA theme generation work.
8
+
3
9
  ## 0.12.1
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,7 @@
1
1
  export { computeZoraHarmony, type ZoraComputedHarmony, type ZoraHarmonySlot, type ZoraHarmonySlotId, } from './harmony';
2
2
  export { parseHexToOklch } from './oklch';
3
3
  export { resolveModePrimaryColor } from './primary';
4
+ export { assignZoraHarmonyRoleHues, getZoraHueRoleAssignment, type ZoraComputedHueRoles, type ZoraHueRoleAssignment, type ZoraHueRoleId, } from './roleHues';
4
5
  export { createZoraColorScale, type CreateZoraColorScaleOptions, createZoraNeutralScale, createZoraPrimaryScale, } from './scales';
5
6
  export { ZORA_COLOR_SCALE_STEPS, type ZoraColorScale, type ZoraColorScaleStep } from './types';
6
7
  //# 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,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,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,6 +1,7 @@
1
1
  export { computeZoraHarmony, } from './harmony';
2
2
  export { parseHexToOklch } from './oklch';
3
3
  export { resolveModePrimaryColor } from './primary';
4
+ export { assignZoraHarmonyRoleHues, getZoraHueRoleAssignment, } from './roleHues';
4
5
  export { createZoraColorScale, createZoraNeutralScale, createZoraPrimaryScale, } from './scales';
5
6
  export { ZORA_COLOR_SCALE_STEPS } from './types';
6
7
  //# 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,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 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,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"]}
@@ -0,0 +1,15 @@
1
+ import type { ZoraColorHarmony } from '../../theme/types';
2
+ import type { ZoraComputedHarmony, ZoraHarmonySlotId } from './harmony';
3
+ export type ZoraHueRoleId = 'primary' | 'secondary' | 'accent' | 'highlight' | 'surfaceTint';
4
+ export interface ZoraHueRoleAssignment {
5
+ role: ZoraHueRoleId;
6
+ hue: number;
7
+ sourceSlotId: ZoraHarmonySlotId;
8
+ }
9
+ export interface ZoraComputedHueRoles {
10
+ harmony: ZoraColorHarmony;
11
+ assignments: readonly ZoraHueRoleAssignment[];
12
+ }
13
+ export declare function assignZoraHarmonyRoleHues(harmony: ZoraComputedHarmony): ZoraComputedHueRoles;
14
+ export declare function getZoraHueRoleAssignment(roles: ZoraComputedHueRoles, role: ZoraHueRoleId): ZoraHueRoleAssignment;
15
+ //# sourceMappingURL=roleHues.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleHues.d.ts","sourceRoot":"","sources":["../../../src/internal/color/roleHues.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,mBAAmB,EAAmB,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGzF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,GAAG,aAAa,CAAC;AAE7F,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,aAAa,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,iBAAiB,CAAC;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAC/C;AAmCD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,mBAAmB,GAAG,oBAAoB,CA8E5F;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,oBAAoB,EAC3B,IAAI,EAAE,aAAa,GAClB,qBAAqB,CAQvB"}
@@ -0,0 +1,103 @@
1
+ import { normalizeHueDegrees } from './hue';
2
+ function findSlot(harmony, id) {
3
+ return harmony.orderedSlots.find((slot) => slot.id === id);
4
+ }
5
+ function requireBaseSlot(harmony) {
6
+ const base = findSlot(harmony, 'base');
7
+ if (!base) {
8
+ throw new Error(`[zora] Expected harmony to include a base slot (kind: ${harmony.kind}).`);
9
+ }
10
+ return base;
11
+ }
12
+ function assignRole(role, harmony, preferred, fallback = 'base') {
13
+ const base = requireBaseSlot(harmony);
14
+ const preferredSlot = findSlot(harmony, preferred);
15
+ const fallbackSlot = findSlot(harmony, fallback);
16
+ const chosenSlot = preferredSlot ?? fallbackSlot ?? base;
17
+ return {
18
+ role,
19
+ hue: normalizeHueDegrees(chosenSlot.hue),
20
+ sourceSlotId: chosenSlot.id,
21
+ };
22
+ }
23
+ export function assignZoraHarmonyRoleHues(harmony) {
24
+ const { kind } = harmony;
25
+ if (kind === 'monochromatic') {
26
+ return {
27
+ harmony: kind,
28
+ assignments: [
29
+ assignRole('primary', harmony, 'base'),
30
+ assignRole('secondary', harmony, 'base'),
31
+ assignRole('accent', harmony, 'base'),
32
+ assignRole('highlight', harmony, 'base'),
33
+ assignRole('surfaceTint', harmony, 'base'),
34
+ ],
35
+ };
36
+ }
37
+ if (kind === 'complementary') {
38
+ return {
39
+ harmony: kind,
40
+ assignments: [
41
+ assignRole('primary', harmony, 'base'),
42
+ assignRole('secondary', harmony, 'base'),
43
+ assignRole('accent', harmony, 'a'),
44
+ assignRole('highlight', harmony, 'a'),
45
+ assignRole('surfaceTint', harmony, 'base'),
46
+ ],
47
+ };
48
+ }
49
+ if (kind === 'analogous') {
50
+ return {
51
+ harmony: kind,
52
+ assignments: [
53
+ assignRole('primary', harmony, 'base'),
54
+ assignRole('secondary', harmony, 'a'),
55
+ assignRole('accent', harmony, 'b', 'a'),
56
+ assignRole('highlight', harmony, 'b', 'a'),
57
+ assignRole('surfaceTint', harmony, 'a'),
58
+ ],
59
+ };
60
+ }
61
+ if (kind === 'splitComplementary') {
62
+ return {
63
+ harmony: kind,
64
+ assignments: [
65
+ assignRole('primary', harmony, 'base'),
66
+ assignRole('secondary', harmony, 'a'),
67
+ assignRole('accent', harmony, 'b', 'a'),
68
+ assignRole('highlight', harmony, 'b', 'a'),
69
+ assignRole('surfaceTint', harmony, 'base'),
70
+ ],
71
+ };
72
+ }
73
+ if (kind === 'triadic') {
74
+ return {
75
+ harmony: kind,
76
+ assignments: [
77
+ assignRole('primary', harmony, 'base'),
78
+ assignRole('secondary', harmony, 'a'),
79
+ assignRole('accent', harmony, 'b', 'a'),
80
+ assignRole('highlight', harmony, 'b', 'a'),
81
+ assignRole('surfaceTint', harmony, 'base'),
82
+ ],
83
+ };
84
+ }
85
+ return {
86
+ harmony: kind,
87
+ assignments: [
88
+ assignRole('primary', harmony, 'base'),
89
+ assignRole('secondary', harmony, 'a'),
90
+ assignRole('accent', harmony, 'b', 'a'),
91
+ assignRole('highlight', harmony, 'c', 'b'),
92
+ assignRole('surfaceTint', harmony, 'base'),
93
+ ],
94
+ };
95
+ }
96
+ export function getZoraHueRoleAssignment(roles, role) {
97
+ const found = roles.assignments.find((assignment) => assignment.role === role);
98
+ if (!found) {
99
+ throw new Error(`[zora] Expected a hue-role assignment for "${role}" (harmony: ${roles.harmony}).`);
100
+ }
101
+ return found;
102
+ }
103
+ //# sourceMappingURL=roleHues.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleHues.js","sourceRoot":"","sources":["../../../src/internal/color/roleHues.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ZoraColorHarmony } from '../../theme/types';\nimport type { ZoraComputedHarmony, ZoraHarmonySlot, ZoraHarmonySlotId } from './harmony';\nimport { normalizeHueDegrees } from './hue';\n\nexport type ZoraHueRoleId = 'primary' | 'secondary' | 'accent' | 'highlight' | 'surfaceTint';\n\nexport interface ZoraHueRoleAssignment {\n role: ZoraHueRoleId;\n hue: number;\n sourceSlotId: ZoraHarmonySlotId;\n}\n\nexport interface ZoraComputedHueRoles {\n harmony: ZoraColorHarmony;\n assignments: readonly ZoraHueRoleAssignment[];\n}\n\nfunction findSlot(\n harmony: ZoraComputedHarmony,\n id: ZoraHarmonySlotId,\n): ZoraHarmonySlot | undefined {\n return harmony.orderedSlots.find((slot) => slot.id === id);\n}\n\nfunction requireBaseSlot(harmony: ZoraComputedHarmony): ZoraHarmonySlot {\n const base = findSlot(harmony, 'base');\n if (!base) {\n throw new Error(`[zora] Expected harmony to include a base slot (kind: ${harmony.kind}).`);\n }\n return base;\n}\n\nfunction assignRole(\n role: ZoraHueRoleId,\n harmony: ZoraComputedHarmony,\n preferred: ZoraHarmonySlotId,\n fallback: ZoraHarmonySlotId = 'base',\n): ZoraHueRoleAssignment {\n const base = requireBaseSlot(harmony);\n const preferredSlot = findSlot(harmony, preferred);\n const fallbackSlot = findSlot(harmony, fallback);\n const chosenSlot = preferredSlot ?? fallbackSlot ?? base;\n\n return {\n role,\n hue: normalizeHueDegrees(chosenSlot.hue),\n sourceSlotId: chosenSlot.id,\n };\n}\n\nexport function assignZoraHarmonyRoleHues(harmony: ZoraComputedHarmony): ZoraComputedHueRoles {\n const { kind } = harmony;\n\n if (kind === 'monochromatic') {\n return {\n harmony: kind,\n assignments: [\n assignRole('primary', harmony, 'base'),\n assignRole('secondary', harmony, 'base'),\n assignRole('accent', harmony, 'base'),\n assignRole('highlight', harmony, 'base'),\n assignRole('surfaceTint', harmony, 'base'),\n ],\n };\n }\n\n if (kind === 'complementary') {\n return {\n harmony: kind,\n assignments: [\n assignRole('primary', harmony, 'base'),\n assignRole('secondary', harmony, 'base'),\n assignRole('accent', harmony, 'a'),\n assignRole('highlight', harmony, 'a'),\n assignRole('surfaceTint', harmony, 'base'),\n ],\n };\n }\n\n if (kind === 'analogous') {\n return {\n harmony: kind,\n assignments: [\n assignRole('primary', harmony, 'base'),\n assignRole('secondary', harmony, 'a'),\n assignRole('accent', harmony, 'b', 'a'),\n assignRole('highlight', harmony, 'b', 'a'),\n assignRole('surfaceTint', harmony, 'a'),\n ],\n };\n }\n\n if (kind === 'splitComplementary') {\n return {\n harmony: kind,\n assignments: [\n assignRole('primary', harmony, 'base'),\n assignRole('secondary', harmony, 'a'),\n assignRole('accent', harmony, 'b', 'a'),\n assignRole('highlight', harmony, 'b', 'a'),\n assignRole('surfaceTint', harmony, 'base'),\n ],\n };\n }\n\n if (kind === 'triadic') {\n return {\n harmony: kind,\n assignments: [\n assignRole('primary', harmony, 'base'),\n assignRole('secondary', harmony, 'a'),\n assignRole('accent', harmony, 'b', 'a'),\n assignRole('highlight', harmony, 'b', 'a'),\n assignRole('surfaceTint', harmony, 'base'),\n ],\n };\n }\n\n return {\n harmony: kind,\n assignments: [\n assignRole('primary', harmony, 'base'),\n assignRole('secondary', harmony, 'a'),\n assignRole('accent', harmony, 'b', 'a'),\n assignRole('highlight', harmony, 'c', 'b'),\n assignRole('surfaceTint', harmony, 'base'),\n ],\n };\n}\n\nexport function getZoraHueRoleAssignment(\n roles: ZoraComputedHueRoles,\n role: ZoraHueRoleId,\n): ZoraHueRoleAssignment {\n const found = roles.assignments.find((assignment) => assignment.role === role);\n if (!found) {\n throw new Error(\n `[zora] Expected a hue-role assignment for \"${role}\" (harmony: ${roles.harmony}).`,\n );\n }\n return found;\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.1",
4
+ "version": "0.12.2",
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": {
@@ -6,6 +6,13 @@ export {
6
6
  } from './harmony';
7
7
  export { parseHexToOklch } from './oklch';
8
8
  export { resolveModePrimaryColor } from './primary';
9
+ export {
10
+ assignZoraHarmonyRoleHues,
11
+ getZoraHueRoleAssignment,
12
+ type ZoraComputedHueRoles,
13
+ type ZoraHueRoleAssignment,
14
+ type ZoraHueRoleId,
15
+ } from './roleHues';
9
16
  export {
10
17
  createZoraColorScale,
11
18
  type CreateZoraColorScaleOptions,
@@ -0,0 +1,197 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ assignZoraHarmonyRoleHues,
5
+ getZoraHueRoleAssignment,
6
+ type ZoraComputedHarmony,
7
+ type ZoraComputedHueRoles,
8
+ type ZoraHueRoleAssignment,
9
+ type ZoraHueRoleId,
10
+ } from './index';
11
+
12
+ function expectNormalizedHue(hue: number) {
13
+ expect(Number.isFinite(hue)).toBe(true);
14
+ expect(hue).toBeGreaterThanOrEqual(0);
15
+ expect(hue).toBeLessThan(360);
16
+ }
17
+
18
+ const ROLE_ORDER: readonly ZoraHueRoleId[] = [
19
+ 'primary',
20
+ 'secondary',
21
+ 'accent',
22
+ 'highlight',
23
+ 'surfaceTint',
24
+ ];
25
+
26
+ function expectEveryRoleOnce(roles: ReturnType<typeof assignZoraHarmonyRoleHues>) {
27
+ expect(roles.assignments).toHaveLength(ROLE_ORDER.length);
28
+ expect(roles.assignments.map((assignment) => assignment.role)).toEqual(ROLE_ORDER);
29
+ }
30
+
31
+ describe('assignZoraHarmonyRoleHues', () => {
32
+ test('monochromatic assigns every role to base', () => {
33
+ const harmony: ZoraComputedHarmony = {
34
+ kind: 'monochromatic',
35
+ orderedSlots: [{ id: 'base', hue: 100 }],
36
+ };
37
+
38
+ const roles: ZoraComputedHueRoles = assignZoraHarmonyRoleHues(harmony);
39
+
40
+ expectEveryRoleOnce(roles);
41
+ for (const assignment of roles.assignments) {
42
+ expect(assignment.sourceSlotId).toBe('base');
43
+ expect(assignment.hue).toBe(100);
44
+ expectNormalizedHue(assignment.hue);
45
+ }
46
+ });
47
+
48
+ test('complementary assigns accent/highlight to a', () => {
49
+ const harmony: ZoraComputedHarmony = {
50
+ kind: 'complementary',
51
+ orderedSlots: [
52
+ { id: 'base', hue: 100 },
53
+ { id: 'a', hue: 280 },
54
+ ],
55
+ };
56
+
57
+ const roles: ZoraComputedHueRoles = assignZoraHarmonyRoleHues(harmony);
58
+
59
+ expectEveryRoleOnce(roles);
60
+ const accent: ZoraHueRoleAssignment = getZoraHueRoleAssignment(roles, 'accent');
61
+ const highlight: ZoraHueRoleAssignment = getZoraHueRoleAssignment(roles, 'highlight');
62
+
63
+ expect(accent.sourceSlotId).toBe('a');
64
+ expect(highlight.sourceSlotId).toBe('a');
65
+ expect(getZoraHueRoleAssignment(roles, 'primary').sourceSlotId).toBe('base');
66
+ expect(getZoraHueRoleAssignment(roles, 'surfaceTint').sourceSlotId).toBe('base');
67
+ });
68
+
69
+ test('analogous assigns secondary/surfaceTint to a and accent/highlight to b', () => {
70
+ const harmony: ZoraComputedHarmony = {
71
+ kind: 'analogous',
72
+ orderedSlots: [
73
+ { id: 'base', hue: 100 },
74
+ { id: 'a', hue: 70 },
75
+ { id: 'b', hue: 130 },
76
+ ],
77
+ };
78
+
79
+ const roles = assignZoraHarmonyRoleHues(harmony);
80
+
81
+ expectEveryRoleOnce(roles);
82
+ expect(getZoraHueRoleAssignment(roles, 'secondary').sourceSlotId).toBe('a');
83
+ expect(getZoraHueRoleAssignment(roles, 'surfaceTint').sourceSlotId).toBe('a');
84
+ expect(getZoraHueRoleAssignment(roles, 'accent').sourceSlotId).toBe('b');
85
+ expect(getZoraHueRoleAssignment(roles, 'highlight').sourceSlotId).toBe('b');
86
+ });
87
+
88
+ test('splitComplementary assigns primary/surfaceTint to base and accent/highlight to b', () => {
89
+ const harmony: ZoraComputedHarmony = {
90
+ kind: 'splitComplementary',
91
+ orderedSlots: [
92
+ { id: 'base', hue: 100 },
93
+ { id: 'a', hue: 250 },
94
+ { id: 'b', hue: 310 },
95
+ ],
96
+ };
97
+
98
+ const roles = assignZoraHarmonyRoleHues(harmony);
99
+
100
+ expectEveryRoleOnce(roles);
101
+ expect(getZoraHueRoleAssignment(roles, 'primary').sourceSlotId).toBe('base');
102
+ expect(getZoraHueRoleAssignment(roles, 'surfaceTint').sourceSlotId).toBe('base');
103
+ expect(getZoraHueRoleAssignment(roles, 'accent').sourceSlotId).toBe('b');
104
+ expect(getZoraHueRoleAssignment(roles, 'highlight').sourceSlotId).toBe('b');
105
+ });
106
+
107
+ test('triadic assigns primary/surfaceTint to base and secondary/accent across a/b', () => {
108
+ const harmony: ZoraComputedHarmony = {
109
+ kind: 'triadic',
110
+ orderedSlots: [
111
+ { id: 'base', hue: 100 },
112
+ { id: 'a', hue: 220 },
113
+ { id: 'b', hue: 340 },
114
+ ],
115
+ };
116
+
117
+ const roles = assignZoraHarmonyRoleHues(harmony);
118
+
119
+ expectEveryRoleOnce(roles);
120
+ expect(getZoraHueRoleAssignment(roles, 'primary').sourceSlotId).toBe('base');
121
+ expect(getZoraHueRoleAssignment(roles, 'surfaceTint').sourceSlotId).toBe('base');
122
+ expect(getZoraHueRoleAssignment(roles, 'secondary').sourceSlotId).toBe('a');
123
+ expect(getZoraHueRoleAssignment(roles, 'accent').sourceSlotId).toBe('b');
124
+ });
125
+
126
+ test('tetradic assigns highlight to c', () => {
127
+ const harmony: ZoraComputedHarmony = {
128
+ kind: 'tetradic',
129
+ orderedSlots: [
130
+ { id: 'base', hue: 100 },
131
+ { id: 'a', hue: 190 },
132
+ { id: 'b', hue: 280 },
133
+ { id: 'c', hue: 10 },
134
+ ],
135
+ };
136
+
137
+ const roles = assignZoraHarmonyRoleHues(harmony);
138
+
139
+ expectEveryRoleOnce(roles);
140
+ expect(getZoraHueRoleAssignment(roles, 'highlight').sourceSlotId).toBe('c');
141
+ });
142
+
143
+ test('every assignment hue is normalized to [0, 360)', () => {
144
+ const harmony: ZoraComputedHarmony = {
145
+ kind: 'complementary',
146
+ orderedSlots: [
147
+ { id: 'base', hue: -10 },
148
+ { id: 'a', hue: 370 },
149
+ ],
150
+ };
151
+
152
+ const roles = assignZoraHarmonyRoleHues(harmony);
153
+
154
+ expectEveryRoleOnce(roles);
155
+ for (const assignment of roles.assignments) {
156
+ expectNormalizedHue(assignment.hue);
157
+ }
158
+ });
159
+
160
+ test('is deterministic for the same computed harmony', () => {
161
+ const harmony: ZoraComputedHarmony = {
162
+ kind: 'analogous',
163
+ orderedSlots: [
164
+ { id: 'base', hue: 100 },
165
+ { id: 'a', hue: 70 },
166
+ { id: 'b', hue: 130 },
167
+ ],
168
+ };
169
+
170
+ expect(assignZoraHarmonyRoleHues(harmony)).toEqual(assignZoraHarmonyRoleHues(harmony));
171
+ });
172
+
173
+ test('falls back when a preferred non-base slot is missing', () => {
174
+ const harmony: ZoraComputedHarmony = {
175
+ kind: 'analogous',
176
+ orderedSlots: [
177
+ { id: 'base', hue: 100 },
178
+ { id: 'a', hue: 70 },
179
+ ],
180
+ };
181
+
182
+ const roles = assignZoraHarmonyRoleHues(harmony);
183
+
184
+ expectEveryRoleOnce(roles);
185
+ expect(getZoraHueRoleAssignment(roles, 'accent').sourceSlotId).toBe('a');
186
+ expect(getZoraHueRoleAssignment(roles, 'highlight').sourceSlotId).toBe('a');
187
+ });
188
+
189
+ test('throws a clear error when the base slot is missing', () => {
190
+ const harmony: ZoraComputedHarmony = {
191
+ kind: 'complementary',
192
+ orderedSlots: [{ id: 'a', hue: 200 }],
193
+ };
194
+
195
+ expect(() => assignZoraHarmonyRoleHues(harmony)).toThrow('base');
196
+ });
197
+ });
@@ -0,0 +1,142 @@
1
+ import type { ZoraColorHarmony } from '../../theme/types';
2
+ import type { ZoraComputedHarmony, ZoraHarmonySlot, ZoraHarmonySlotId } from './harmony';
3
+ import { normalizeHueDegrees } from './hue';
4
+
5
+ export type ZoraHueRoleId = 'primary' | 'secondary' | 'accent' | 'highlight' | 'surfaceTint';
6
+
7
+ export interface ZoraHueRoleAssignment {
8
+ role: ZoraHueRoleId;
9
+ hue: number;
10
+ sourceSlotId: ZoraHarmonySlotId;
11
+ }
12
+
13
+ export interface ZoraComputedHueRoles {
14
+ harmony: ZoraColorHarmony;
15
+ assignments: readonly ZoraHueRoleAssignment[];
16
+ }
17
+
18
+ function findSlot(
19
+ harmony: ZoraComputedHarmony,
20
+ id: ZoraHarmonySlotId,
21
+ ): ZoraHarmonySlot | undefined {
22
+ return harmony.orderedSlots.find((slot) => slot.id === id);
23
+ }
24
+
25
+ function requireBaseSlot(harmony: ZoraComputedHarmony): ZoraHarmonySlot {
26
+ const base = findSlot(harmony, 'base');
27
+ if (!base) {
28
+ throw new Error(`[zora] Expected harmony to include a base slot (kind: ${harmony.kind}).`);
29
+ }
30
+ return base;
31
+ }
32
+
33
+ function assignRole(
34
+ role: ZoraHueRoleId,
35
+ harmony: ZoraComputedHarmony,
36
+ preferred: ZoraHarmonySlotId,
37
+ fallback: ZoraHarmonySlotId = 'base',
38
+ ): ZoraHueRoleAssignment {
39
+ const base = requireBaseSlot(harmony);
40
+ const preferredSlot = findSlot(harmony, preferred);
41
+ const fallbackSlot = findSlot(harmony, fallback);
42
+ const chosenSlot = preferredSlot ?? fallbackSlot ?? base;
43
+
44
+ return {
45
+ role,
46
+ hue: normalizeHueDegrees(chosenSlot.hue),
47
+ sourceSlotId: chosenSlot.id,
48
+ };
49
+ }
50
+
51
+ export function assignZoraHarmonyRoleHues(harmony: ZoraComputedHarmony): ZoraComputedHueRoles {
52
+ const { kind } = harmony;
53
+
54
+ if (kind === 'monochromatic') {
55
+ return {
56
+ harmony: kind,
57
+ assignments: [
58
+ assignRole('primary', harmony, 'base'),
59
+ assignRole('secondary', harmony, 'base'),
60
+ assignRole('accent', harmony, 'base'),
61
+ assignRole('highlight', harmony, 'base'),
62
+ assignRole('surfaceTint', harmony, 'base'),
63
+ ],
64
+ };
65
+ }
66
+
67
+ if (kind === 'complementary') {
68
+ return {
69
+ harmony: kind,
70
+ assignments: [
71
+ assignRole('primary', harmony, 'base'),
72
+ assignRole('secondary', harmony, 'base'),
73
+ assignRole('accent', harmony, 'a'),
74
+ assignRole('highlight', harmony, 'a'),
75
+ assignRole('surfaceTint', harmony, 'base'),
76
+ ],
77
+ };
78
+ }
79
+
80
+ if (kind === 'analogous') {
81
+ return {
82
+ harmony: kind,
83
+ assignments: [
84
+ assignRole('primary', harmony, 'base'),
85
+ assignRole('secondary', harmony, 'a'),
86
+ assignRole('accent', harmony, 'b', 'a'),
87
+ assignRole('highlight', harmony, 'b', 'a'),
88
+ assignRole('surfaceTint', harmony, 'a'),
89
+ ],
90
+ };
91
+ }
92
+
93
+ if (kind === 'splitComplementary') {
94
+ return {
95
+ harmony: kind,
96
+ assignments: [
97
+ assignRole('primary', harmony, 'base'),
98
+ assignRole('secondary', harmony, 'a'),
99
+ assignRole('accent', harmony, 'b', 'a'),
100
+ assignRole('highlight', harmony, 'b', 'a'),
101
+ assignRole('surfaceTint', harmony, 'base'),
102
+ ],
103
+ };
104
+ }
105
+
106
+ if (kind === 'triadic') {
107
+ return {
108
+ harmony: kind,
109
+ assignments: [
110
+ assignRole('primary', harmony, 'base'),
111
+ assignRole('secondary', harmony, 'a'),
112
+ assignRole('accent', harmony, 'b', 'a'),
113
+ assignRole('highlight', harmony, 'b', 'a'),
114
+ assignRole('surfaceTint', harmony, 'base'),
115
+ ],
116
+ };
117
+ }
118
+
119
+ return {
120
+ harmony: kind,
121
+ assignments: [
122
+ assignRole('primary', harmony, 'base'),
123
+ assignRole('secondary', harmony, 'a'),
124
+ assignRole('accent', harmony, 'b', 'a'),
125
+ assignRole('highlight', harmony, 'c', 'b'),
126
+ assignRole('surfaceTint', harmony, 'base'),
127
+ ],
128
+ };
129
+ }
130
+
131
+ export function getZoraHueRoleAssignment(
132
+ roles: ZoraComputedHueRoles,
133
+ role: ZoraHueRoleId,
134
+ ): ZoraHueRoleAssignment {
135
+ const found = roles.assignments.find((assignment) => assignment.role === role);
136
+ if (!found) {
137
+ throw new Error(
138
+ `[zora] Expected a hue-role assignment for "${role}" (harmony: ${roles.harmony}).`,
139
+ );
140
+ }
141
+ return found;
142
+ }