feat(ColorPicker): add a Glaze-backed color input - #1287
Merged
Conversation
The field shows the current color as a swatch, reads and writes hex, `rgb()`, `hsl()`, `okhsl()`, `okhst()` and `oklch()` text, and opens a popover that tunes the color on HST, LCH or RGB axes. The canonical value is OKHSL — every channel bounded, every value inside the sRGB gamut — so an unrepresentable state is impossible and the value re-serializes into any notation without drift. `formatMode` decides how tightly the text follows `format`: `forced` rewrites it, `derive` keeps the author's notation but normalizes the value, `free` passes the text through after verifying it parses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 7f35b9d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
📦 NPM canary releaseDeployed canary version 0.0.0-canary-ce91e8d. |
Contributor
🧪 Storybook is successfully deployed!
|
Contributor
🏋️ Size limit report
Click here if you want to find out what is changed in this build |
`\d+\.?\d*` lets its two quantifiers split a digit run n ways, so rejecting a long one was quadratic — 818ms for 20k digits, on text the user types. `\d+(?:\.\d*)?` can only split it one way: 0.04ms, with identical accept/reject behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`TextInputBase` defaults `suffixPosition` to `before`, which put the pipette ahead of the validation state. `DateInputBase` already orders it the other way for the date pickers, so the picker family now matches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The string is the unit of editing — a channel is tuned with the sliders, not by hand-editing one number inside `oklch(…)` — so focusing offers the whole value up for replacement, the way a hex field does. Browsers disagree on when a click's caret lands, so the selection is reapplied on release when it comes back collapsed. A dragged range is left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chrome applies a click's caret after the focus handler has run, so selecting there was immediately undone. Suppressing the default action of the press that takes focus means no caret is placed at all; focus moves programmatically instead, and the selection stands. Only that first press is suppressed, so clicking an already-focused field still positions the caret. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves the `.size-limit.cjs` conflict: Board selection and `ColorPicker` both raised the `All` budget from 462 kB, so the comment now records both and the limit is set from a fresh measurement of the two together — 467.87 kB, limit 469 kB. Tree shaking is unchanged at 121.8 kB, which is the check that matters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 898e714. Configure here.
`useTextField` was handed the raw `onFocus` prop, so `handleFocus` never ran and nothing selected the value — the whole select-on-focus path was dead. Caught by Bugbot on #1287. The test that was supposed to cover this asserted the resulting selection range after a Tab, which tabbing into a text input produces natively — so it passed against dead code. It now spies on the call and covers the pointer path too, and fails if the handler is unwired again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Adds
ColorPicker, a form-attachable color input.The field shows the current color as a swatch, accepts hex,
rgb(),hsl(),okhsl(),okhst()andoklch()text, and opens a popover that tunes the color on three sets of axes. Changing anything in the popover writes straight back into the input.The color model
Every conversion runs through Glaze's exported color math (
srgbToOkhsl,okhstToOkhsl,oklabToOkhsl,okhslToOklch, theformat*writers,parseHexAlpha,relativeLuminanceFromLinearRgb). The canonical value is OKHSL: every channel bounded, and every value inside those bounds is inside the sRGB gamut. That makes an unrepresentable state impossible, and lets the same value be re-serialized into any supported notation without drift — covered by a round-trip test across all six formats.Two places needed care:
oklabToOkhsldoes not invert correctly above the gamut boundary (it mapsoklch(0.8 0.4 200)tos: 0.348, which converts back toC ≈ 0.05).fromOklchtherefore clips chroma to the gamut at the requested lightness, and takes hue from the input rather than from the conversion, which loses it at zero chroma.#808080→okhsl(89.88 …)). Formatting zeroes it. CSS HSL needs an extra step, because it derives hue from the sRGB channels rather than carrying it through.Format modes
formatModedecides how tightly the input text followsformat(defaulthex):forced(default)formaton blur / EnterformatderivefreeIn all three, an entry that is not a color is rejected: the previous valid color stays the value while it is being typed, and the text snaps back to it on blur. Emptying the field is not an error — it commits
null.The popover
Concise by design: a preview whose caption flips between black and white by measured WCAG contrast, a tabs switcher, and one gradient slider per channel of the active space.
His the same angle in HST and LCH, so the hue strip is shared. The chroma slider's ceiling is the gamut boundary at the current lightness and hue, so it has no dead zone and never snaps back mid-drag. Padding is1x.Each track's gradient samples the real conversion (7 stops for the perceptual channels, 25 for the hue strip, 2 for sRGB channels, which are linear in the space CSS interpolates in). Gradients and colors reach the DOM as inline custom properties rather than fresh
stylesobjects — a color picker moves continuously, and a new style object per frame would emit a new CSS rule per frame.Notes
red) are not accepted, matchingglaze.color(). Alpha parses but is dropped.PipetteIcon, and#color-picker/#color-picker-contrastto the component-local tokens intasty.config.ts.Allsize-limit budget goes 462 → 465 kB (measured 463.94 kB). Tree-shaking is unchanged at 121.8 kB.🤖 Generated with Claude Code
Note
Low Risk
Additive field API with broad unit tests; main impact is ~3.8 kB on the full bundle for consumers who import ColorPicker, with no changes to auth or existing field contracts.
Overview
Adds
ColorPicker, a form field with a swatch, typed color text, and a popover to tune the value on OKHST, OKLCH, or RGB sliders. Parsing and serialization go through Glaze so values stay in-gamut sRGB; supported notations include hex,rgb(),hsl(),okhsl(),okhst(), andoklch()(named CSS colors and alpha are rejected or dropped).formatandformatMode(forced/derive/free) control how display text relates toonChange: rewrite to a fixed format, keep the user’s notation while normalizing the color, or pass text through verbatim when it parses. Invalid partial input keeps the last good color until blur/Enter settles or reverts.Also exports
PipetteIcon, registers#color-picker/#color-picker-contrastintasty.config.ts, re-exports fromfields, and documents the API in Storybook/MDX. The all-bundle size budget rises to 469 kB (~3.8 kB for the picker). A changeset marks@cube-dev/ui-kitminor. The ui-kit-verification Claude skill doc is removed from the repo.Reviewed by Cursor Bugbot for commit 7f35b9d. Bugbot is set up for automated code reviews on this repo. Configure here.