GrColorPicker
Берут, когда цвет задаёт пользователь.
Обзор
Когда брать
- цвет задаёт пользователь — тема бренда, метка проекта, цвет категории;
- нужен точный код — hex-поле рядом с каналами: цвет чаще приносят из макета, а не подбирают;
- есть фирменный набор —
presetsпоказывает палитру, из которой выбирают в девяти случаях из десяти; - нужна прозрачность —
alphaдобавляет канал и меняет формат значения.
Когда взять другое
| Нужно | Берите |
|---|---|
| Выбор из нескольких фиксированных цветов | GrRadioGroup / GrSegmented |
| Цвет — тон компонента из палитры темы | проп tone нужного компонента |
| Значение — произвольная строка | GrInput |
Варианты все тона и виды — настоящим компонентом
view
slidersarea
Размеры
xssmmdlg
Состояния
обычнаяdisabledreadonlyinvalid
Матрица живая: наводите и нажимайте. Состояний наведения, фокуса и нажатия здесь нет — они за браузером, а не за пропом.
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrColorPicker>Текст</GrColorPicker>Примеры 3 у каждого — живое демо и его код
Фирменный цвет, который обязан оставаться читаемым
Значение — hex-строка: её читают CSS, токены и сервер.
view="area"даёт квадрат насыщенность × светлота,presets— фирменную палитру,eyedropper— пипетку там, где она есть у браузера, аnameотправляет значение с нативной формой. Белому тексту нужно 4,5:1, поэтому цвет, который не проходит, —invalid, а строка под ним говорит почему; превью показывает, куда цвет попадёт.BrandingShown on invoices, payment pages and emails White text 5.5:1 — readableAcme BillingInvoice INV-0418Amount due€1,284.50Цвета меток в списке
Маленький пикер
pillпомещается в строку списка, аplacement="bottom-end"раскрывает панель внутрь от правого края. Системная метка —readonly: панель открывается и показывает цвет, но значение не меняется.LabelsBilling web · project board - Bug · systemТолько для чтения
- Design
- Backend
- Waiting on customer
- Bug · system
Вуаль с прозрачностью
alphaдобавляет четвёртый бегунок и делает значение#RRGGBBAA, которое CSS читает как есть: фото просвечивает сквозь вуаль, а заголовок остаётся читаемым. Пресеты несут свою прозрачность.Sign-in pageOverlay over the background photo Welcome back#0f172abf
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
placement | "bottom-start" | "bottom-end" | "top-start" | "top-end" | undefined | "bottom-start" | Сторона, с которой раскрывается панель. |
disabled | boolean | undefined | false | Поле недоступно: триггер выключен, панель не открывается. Включается и `disabled` у `GrFormField`. |
openv-model | boolean | undefined | undefined | Контролируемое состояние панели (`v-model:open`). |
readonly | boolean | undefined | false | Только для чтения: цвет видно, панель открывается, но значение не меняется. |
invalid | boolean | undefined | false | Состояние ошибки: рамка цвета ошибки и `aria-invalid`. Включается и ошибкой `GrFormField`. |
required | boolean | undefined | false | Обязательное поле. `aria-required` у кнопки-триггера невалиден, поэтому обязательность уходит в её описание для скринридера. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Высота триггера и масштаб панели по шкале контролов. Не задан — из `GrConfigProvider`, иначе `md`. |
ariaLabel | string | undefined | undefined | Доступное имя триггера вне `GrFormField` — когда видимой подписи рядом нет. |
name | string | undefined | undefined | Имя для нативной формы: значение уходит скрытым полем. |
shape | "box" | "pill" | undefined | undefined | Форма рамки. `box` — скругление шкалы контролов; `pill` — пилюля. |
alpha | boolean | undefined | false | Четвёртый слайдер и восьмизначная форма hex. |
view | "sliders" | "area" | undefined | undefined | Как задаются насыщенность и светлота: двумя бегунками или квадратом. Оттенок и прозрачность остаются бегунками в обоих видах. |
eyedropper | boolean | undefined | undefined | Кнопка пипетки. Рисуется только там, где браузер даёт `EyeDropper`, — сегодня это Chromium. |
presets | string[] | undefined | [] | Палитра быстрого выбора. Пусто — блок не рендерится. |
modelValueобязательныйv-model | string | — | Цвет в hex: `#RRGGBB`, а при `alpha` — `#RRGGBBAA`. Мусор не роняет компонент. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: string): void | — |
change | (event: "change", value: string): void | — |
update:open | (event: "update:open", value: boolean): void | — |
focus | (event: "focus", event: FocusEvent): void | — |
blur | (event: "blur", event: FocusEvent): void | — |
Методы
| Methods | Сигнатура | Описание |
|---|---|---|
focus | () => void | Фокус на кнопку-триггер из кода. |
blur | () => void | Снять фокус с кнопки-триггера. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrColorPickerConfigurableProps, GrColorPickerProps, GrColorPickerEmits } from '@feugene/granularity/components/GrColorPicker'Файлdist/types/components/GrColorPicker/config.d.ts
const grColorPickerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrColorPicker">;Файлdist/types/components/GrColorPicker/defaults.d.ts
Пропы GrColorPicker, настраиваемые глобально через componentDefaults.
Только оформление: значение, пресеты и состояние панели принадлежат
конкретному экземпляру.
interface GrColorPickerConfigurableProps {
size: GrColorPickerSize;
/** Форма рамки — см. `components/shared/controlShape.ts`. */
shape: GrControlShape;
view: GrColorPickerView;
eyedropper: boolean;
}Файлdist/types/components/GrColorPicker/GrColorPicker.vue.d.ts
interface GrColorPickerProps {
/** Цвет в hex: `#RRGGBB`, а при `alpha` — `#RRGGBBAA`. Мусор не роняет компонент. */
modelValue: string;
/** Четвёртый слайдер и восьмизначная форма hex. */
alpha?: boolean;
/**
* Как задаются насыщенность и светлота: двумя бегунками или квадратом.
* Оттенок и прозрачность остаются бегунками в обоих видах.
*/
view?: GrColorPickerView;
/**
* Кнопка пипетки. Рисуется только там, где браузер даёт `EyeDropper`, —
* сегодня это Chromium.
*/
eyedropper?: boolean;
/** Палитра быстрого выбора. Пусто — блок не рендерится. */
presets?: string[];
/**
* Высота триггера и масштаб панели по шкале контролов.
* Не задан — из `GrConfigProvider`, иначе `md`.
*/
size?: GrColorPickerSize;
/** Форма рамки. `box` — скругление шкалы контролов; `pill` — пилюля. */
shape?: GrControlShape;
/** Контролируемое состояние панели (`v-model:open`). */
open?: boolean;
/** Сторона, с которой раскрывается панель. */
placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
/** Имя для нативной формы: значение уходит скрытым полем. */
name?: string;
/**
* Поле недоступно: триггер выключен, панель не открывается.
* Включается и `disabled` у `GrFormField`.
*/
disabled?: boolean;
/** Только для чтения: цвет видно, панель открывается, но значение не меняется. */
readonly?: boolean;
/** Состояние ошибки: рамка цвета ошибки и `aria-invalid`. Включается и ошибкой `GrFormField`. */
invalid?: boolean;
/**
* Обязательное поле. `aria-required` у кнопки-триггера невалиден, поэтому
* обязательность уходит в её описание для скринридера.
*/
required?: boolean;
/** Доступное имя триггера вне `GrFormField` — когда видимой подписи рядом нет. */
ariaLabel?: string;
}interface GrColorPickerEmits {
/** Новый цвет в hex — на каждом шаге бегунка, выборе из палитры и валидном вводе в поле hex. */
(e: 'update:modelValue', value: string): void;
/** Цвет изменился — тот же hex и в те же моменты, что у `update:modelValue`. */
(e: 'change', value: string): void;
/** Панель открылась или закрылась (`v-model:open`). */
(e: 'update:open', value: boolean): void;
/** Фокус пришёл на кнопку-триггер. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл с кнопки-триггера — в том числе в открытую панель. */
(e: 'blur', event: FocusEvent): void;
}Файлdist/types/components/GrColorPicker/grColorPickerStyles.d.ts
type GrColorPickerSize = GrComponentSize;Файлdist/types/components/GrColorPicker/color.d.ts
Арифметика цвета для GrColorPicker — чистый модуль без Vue.
Модель компонента — hex-строка (в такой форме цвет лежит в токенах темы),
а панель работает в HSL: у оттенка, насыщенности и светлоты есть шкала, по
которой можно вести слайдер, а у каналов RGB её нет.
Свой парсер, а не __tests__/cssContrast.ts: тот — инфраструктура гейтов, и
тянуть тестовый модуль в поставляемый код нельзя.
type GrHsla = {
/** Оттенок, 0…359 (круговой: 360 — это 0). */
h: number;
/** Насыщенность, 0…100. */
s: number;
/** Светлота, 0…100. */
l: number;
/** Непрозрачность, 0…1. */
a: number;
};Файлdist/types/components/GrColorPicker/safelist.d.ts
const grColorPickerSafelist: string[];Файлdist/types/components/GrColorPicker/index.d.ts
type GrColorPickerInstance = ComponentExposed<typeof GrColorPickerComponent>;Доступность
Паттерн APG
dialog + slider
Клавиши
EscTab↑ / ↓
| Клавиша | Действие |
|---|---|
| — | Enter/Space на триггере — открыть панель |
| Esc | закрыть и вернуть фокус на триггер |
| Tab | уйти из панели дальше по странице; внутри каналов — клавиши GrSlider. При view="area" вместо бегунков S и L стоит квадрат: ←/→ — насыщенность |
| ↑ / ↓ | светлота |
| — | Home/End и PageUp/PageDown — по оси сфокусированного поля. Осей две, и остановок Tab тоже две (как у диапазона GrSlider); стрелка поперёк оси уводит фокус на поле изменившегося канала — иначе диктор промолчал бы о нём |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
Модель — hex-строка
modelValue — #RRGGBB, а при alpha — #RRGGBBAA. Это та форма, в которой
цвет лежит в токенах темы, приезжает с бэкенда и понимается CSS: потребителю не
приходится конвертировать ни на входе, ни на выходе.
Невалидное значение не роняет компонент и ничего не эмитит: панель показывает
#000000, модель остаётся как есть. Молча переписывать чужие данные компонент
не вправе.
Внутри цвет живёт в HSLA и отдельно от модели, потому что hex — проекция с потерями: у чёрного, белого и любого серого нет оттенка. Держи компонент состояние только в hex — бегунок оттенка прыгал бы на 0° каждый раз, когда пользователь уводит насыщенность в ноль.
Каналы при этом хранятся дробными. Целая сетка HSL мельче сетки hex, и посадка
на неё меняет цвет: из двенадцати типовых брендовых значений одиннадцать
возвращались бы другими — #10b981 как #10b77f. Целые нужны шкалам, и их
получают шкалы: бегунок читает округление своего канала, а хранимое значение
остаётся точным. Поэтому выбранный пресет, вставленный hex и взятый пипеткой
цвет возвращаются ровно теми же.
Два вида панели
view решает, чем задаются насыщенность и светлота. Оттенок и прозрачность —
бегунки в обоих видах.
view | Что в панели |
|---|---|
sliders (по умолчанию) | лестница каналов: H, S, L и A — каждый GrSlider |
area | квадрат насыщенность × светлота вместо S и L |
Дефолт остался прежним намеренно: вид меняет облик уже стоящих в приложениях
панелей, и решение это приложения, а не пакета. Переключается он и глобально —
<GrConfigProvider :component-defaults="{ GrColorPicker: { view: 'area' } }">.
Квадрат озвучен, а не только нарисован
Обычная беда 2D-области — что она мышиная: у неё нет ни значений, которые
прочитает диктор, ни клавиш. Здесь внутри квадрата два настоящих
input[type=range], по одному на ось: они визуально скрыты, но остаются в
таб-порядке и со своими именами и значениями, а фокус показывает сама область
через focus-within. Диктор видит два слайдера — «Saturation 90 %» и
«Lightness 66 %», — а не безымянную картинку.
Отсюда и две остановки Tab на квадрат: осей две, как бегунков у диапазона
GrSlider. Клавиши — ../keyboard.md.
Заливка квадрата точна, а не похожа
Квадрат собран тремя слоями: шкала насыщенности при светлоте 50 % и пара вуалей, белой сверху и чёрной снизу. Это не приём «на глаз»: осветление и затемнение в HSL — линейная интерполяция к белому и к чёрному, ровно то, что делает наложение с альфой. Цвет под ручкой совпадает с hex до последнего разряда, и это проверяется пикселем в живом браузере.
Оси — насыщенность и светлота, а не привычные по HSV насыщенность и яркость. Перевод в HSV дал бы более нарядную заливку, но принёс бы вторую проекцию с потерями: при нулевой яркости насыщенность HSV не определена, и чёрный терял бы её так же, как серый теряет оттенок.
Пипетка
eyedropper добавляет кнопку рядом с hex-полем. Она рисуется только там, где
браузер даёт EyeDropper — сегодня это Chromium; в остальных её нет вовсе, а
не есть и не работает. Отказ пользователя (Esc во время выбора) ничего не
меняет и ни о чём не сообщает: это не ошибка.
С экрана приходит уже смешанный цвет, своей прозрачности у него нет — текущая остаётся как была.
Панель немодальная
Tab из панели уводит фокус дальше по странице, а не запирает в ней: за
страницей ничего не блокируется, и запирать пользователя не за что. Esc
закрывает панель и возвращает фокус на триггер — это делает общий стек слоёв
через GrPopover.
Открытием можно управлять снаружи: v-model:open, положение — placement.
Пресеты
presets — массив hex-строк. Невалидные отсеиваются, у выбранного пресета
aria-pressed. Пусто — блока пресетов нет вовсе.
Токены темы в presets не подставляются: --gr-primary живёт CSS-переменной и
на сборке в hex не разрешается. Палитру собирает приложение — из своего
конфига или из getComputedStyle.
Форма
Контракт форм-контрола целиком: disabled, readonly, invalid, required,
ariaLabel, эмиты update:modelValue/change/focus/blur, экспортируемые
focus()/blur(). Имя берётся от GrFormField (<label for> указывает на
триггер) либо из ariaLabel.
name отдаёт текущее значение в нативную форму скрытым полем — внутри виджета
интерактивных элементов быть не должно.
clearable нет намеренно: у цвета не бывает пустого состояния. Нужна
«не задано» — это undefined в модели на уровне приложения, а не состояние
контрола.
Границы
- истории недавних цветов нет — это состояние приложения;
- градиентов нет — компонент про один цвет.
Пропы этого примера