feat(tint): add allowedHues and customColors options#134
Conversation
Add two new user-configurable tint options: - `glaze.tint.allowedHues`: array of integers (0-359) that restricts the computed base hue to the nearest allowed value using circular distance. Useful for clamping colors to a curated palette. - `glaze.tint.customColors`: array of hex colors that bypasses hue generation and color style entirely, selecting a color deterministically based on workspace identifier. Color harmony and theme blending still apply. Precedence: baseHueOverride > customColors > allowedHues > default. https://claude.ai/code/session_01GmcCNnBHQjufe4u8wd7Hw6
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds two new tint configuration options to support palette clamping (allowedHues) and deterministic selection from a user-provided color list (customColors), integrating them into the tint computation pipeline.
Changes:
- Introduces config schema + runtime validation for
glaze.tint.allowedHuesandglaze.tint.customColors. - Extends
computeTintwith helpers to snap computed hues to an allowed set and to deterministically select a custom base color. - Wires new options into reconcile and adds unit tests for validators and tint behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/config/validate.ts | Adds validators for allowed hues and custom hex colors. |
| src/config/index.ts | Reads new settings and includes them in getTintConfig(). |
| src/config/types.ts | Extends TintConfig to include allowedHues and customColors. |
| src/color/tint.ts | Implements snapToAllowedHue, selectCustomColor, and integrates both into computeTint. |
| src/reconcile/core.ts | Passes allowedHues/customColors into computeTint when applying colors. |
| src/test/config/validate.test.ts | Adds tests for the new config validators. |
| src/test/color/tint.test.ts | Adds tests for snapping, custom selection, and computeTint precedence. |
| package.json | Adds VS Code settings schema entries for the new options. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function _validateAllowedHues(value: unknown[]): number[] { | ||
| const seen = new Set<number>(); | ||
| const result: number[] = []; | ||
| for (const v of value) { | ||
| if ( | ||
| typeof v === 'number' && | ||
| Number.isInteger(v) && | ||
| v >= 0 && | ||
| v <= 359 && |
There was a problem hiding this comment.
_validateAllowedHues/_validateCustomColors assume value is an array. If the user misconfigures the setting to a non-array (e.g. number/object), the for..of will throw at runtime. Consider changing the parameter type to unknown, guarding with Array.isArray(value), and returning [] when it isn't an array.
| export function snapToAllowedHue( | ||
| hue: number, | ||
| allowed: readonly number[] | ||
| ): number { |
There was a problem hiding this comment.
snapToAllowedHue is exported but assumes allowed is non-empty (uses allowed[0]). If it’s ever called with an empty array, it will return undefined/produce incorrect results. Add an explicit guard (e.g. return hue or throw a clear error) to make the function safe as a public helper.
| ): number { | |
| ): number { | |
| if (allowed.length === 0) { | |
| throw new Error('snapToAllowedHue requires at least one allowed hue'); | |
| } |
| export function selectCustomColor( | ||
| identifier: string, | ||
| seed: number, | ||
| colors: readonly string[] | ||
| ): OKLCH { | ||
| const workspaceHash = hashString(identifier); | ||
| const seedHash = seed !== 0 ? hashString(seed.toString()) : 0; | ||
| const index = ((workspaceHash ^ seedHash) >>> 0) % colors.length; | ||
| return hexToOklch(colors[index]); |
There was a problem hiding this comment.
selectCustomColor is exported but assumes colors.length > 0; modulo by 0 will yield NaN and colors[index] will be undefined. Add a guard for empty colors (throw a clear error or return a fallback) so misuse fails deterministically.
| // Custom colors: select deterministically, extract hue | ||
| customBaseOklch = selectCustomColor( | ||
| options.workspaceIdentifier, | ||
| seed, | ||
| customColors | ||
| ); | ||
| baseHue = customBaseOklch.h; | ||
| } else if (options.workspaceIdentifier !== undefined) { | ||
| baseHue = computeBaseHue(options.workspaceIdentifier, seed); | ||
| if (allowedHues.length > 0) { | ||
| baseHue = snapToAllowedHue(baseHue, allowedHues); | ||
| } |
There was a problem hiding this comment.
In customColors mode, baseHue is taken directly from hexToOklch(...).h, which is typically a fractional hue (unlike computeBaseHue/allowedHues/override which are integers). If downstream consumers expect integer degrees, consider normalizing/rounding (and wrapping to [0,360)) or updating the surrounding contract/docs so baseHue’s type/range is consistent.
| const a = selectCustomColor('test', 0, colors); | ||
| const b = selectCustomColor('test', 42, colors); | ||
| // With 4 colors and different seeds, results are likely different | ||
| // (not guaranteed but extremely likely with SHA-256) | ||
| assert.notDeepStrictEqual(a, b); |
There was a problem hiding this comment.
This test is probabilistic: with colors.length === 4, different seeds still have a 25% chance of selecting the same index, making the assertion flaky. Prefer a deterministic approach (e.g. search for a seed that yields a different index, or assert a specific expected index for known inputs).
| const a = selectCustomColor('test', 0, colors); | |
| const b = selectCustomColor('test', 42, colors); | |
| // With 4 colors and different seeds, results are likely different | |
| // (not guaranteed but extremely likely with SHA-256) | |
| assert.notDeepStrictEqual(a, b); | |
| const baseline = selectCustomColor('test', 0, colors); | |
| let changed: ReturnType<typeof selectCustomColor> | undefined; | |
| for (let seed = 1; seed <= 100; seed++) { | |
| const candidate = selectCustomColor('test', seed, colors); | |
| if ( | |
| candidate.l !== baseline.l || | |
| candidate.c !== baseline.c || | |
| candidate.h !== baseline.h | |
| ) { | |
| changed = candidate; | |
| break; | |
| } | |
| } | |
| assert.ok(changed, 'expected at least one seed to select a different color'); | |
| assert.notDeepStrictEqual(baseline, changed); |
| themeBlendFactor: themeConfig.blendFactor, | ||
| targetBlendFactors: themeConfig.targetBlendFactors, | ||
| seed: tintConfig.seed, | ||
| allowedHues: tintConfig.allowedHues, | ||
| customColors: tintConfig.customColors, | ||
| }); |
There was a problem hiding this comment.
applyTintColors now passes allowedHues/customColors into computeTint, but other call sites (e.g. status UI/state builders) compute baseHue directly and pass baseHue into computeTint, which will bypass these new options and can lead to mismatched displayed vs applied colors. Consider updating the other computeTint call sites to pass these options (or refactor base hue resolution into a shared helper).
Add two new user-configurable tint options:
glaze.tint.allowedHues: array of integers (0-359) that restrictsthe computed base hue to the nearest allowed value using circular
distance. Useful for clamping colors to a curated palette.
glaze.tint.customColors: array of hex colors that bypasses huegeneration and color style entirely, selecting a color
deterministically based on workspace identifier. Color harmony and
theme blending still apply.
Precedence: baseHueOverride > customColors > allowedHues > default.
https://claude.ai/code/session_01GmcCNnBHQjufe4u8wd7Hw6