GrColorPicker
Берут, когда цвет задаёт пользователь.
Когда брать
- цвет задаёт пользователь — тема бренда, метка проекта, цвет категории;
- нужен точный код — hex-поле рядом с каналами: цвет чаще приносят из макета, а не подбирают;
- есть фирменный набор —
presetsпоказывает палитру, из которой выбирают в девяти случаях из десяти; - нужна прозрачность —
alphaдобавляет канал и меняет формат значения.
Когда взять другое
| Нужно | Берите |
|---|---|
| Выбор из нескольких фиксированных цветов | GrRadioGroup / GrSegmented |
| Цвет — тон компонента из палитры темы | проп tone нужного компонента |
| Значение — произвольная строка | GrInput |
Модель — hex-строка
modelValue — #RRGGBB, а при alpha — #RRGGBBAA. Это та форма, в которой
цвет лежит в токенах темы, приезжает с бэкенда и понимается CSS: потребителю не
приходится конвертировать ни на входе, ни на выходе.
Невалидное значение не роняет компонент и ничего не эмитит: панель показывает
#000000, модель остаётся как есть. Молча переписывать чужие данные компонент
не вправе.
Внутри цвет живёт в HSLA и отдельно от модели, потому что hex — проекция с потерями: у чёрного, белого и любого серого нет оттенка. Держи компонент состояние только в hex — бегунок оттенка прыгал бы на 0° каждый раз, когда пользователь уводит насыщенность в ноль.
Почему слайдеры, а не квадрат
Привычная 2D-область saturation/value — отдельный виджет со своими жестами и
клавиатурой по двум осям, и доступность в нём приходится собирать с нуля.
Каналы здесь — обычные GrSlider, то есть настоящий role="slider" с полной
клавиатурой, aria-valuetext и aria-label из локали. Цена — на один жест
больше; выигрыш — работающая клавиатура и диктор.
Панель немодальная
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 в модели на уровне приложения, а не состояние
контрола.
Границы
- пипетки нет —
EyeDropperесть не во всех браузерах и требует своего разрешения; - истории недавних цветов нет — это состояние приложения;
- градиентов нет — компонент про один цвет.
Playground 11
Загружается…
<GrColorPicker />Установка
npm i @feugene/granularityИмпорт
import { GrColorPicker } from '@feugene/granularity/components/GrColorPicker'API
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
open | boolean | undefined | undefined | Контролируемое состояние панели (`v-model:open`). |
disabled | boolean | undefined | false | — |
readonly | boolean | undefined | false | Только для чтения: цвет видно, панель открывается, но значение не меняется. |
invalid | boolean | undefined | false | — |
required | boolean | undefined | false | — |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | — |
ariaLabel | string | undefined | undefined | — |
name | string | undefined | undefined | Имя для нативной формы: значение уходит скрытым полем. |
placement | "bottom-start" | "bottom-end" | "top-start" | "top-end" | undefined | "bottom-start" | Сторона, с которой раскрывается панель. |
alpha | boolean | undefined | false | Четвёртый слайдер и восьмизначная форма hex. |
presets | string[] | undefined | [] | Палитра быстрого выбора. Пусто — блок не рендерится. |
modelValueобязательный | string | — | Цвет в hex: `#RRGGBB`, а при `alpha` — `#RRGGBBAA`. Мусор не роняет компонент. |
Events
| Event | Type | Описание |
|---|---|---|
update:modelValue | [value: string] | — |
change | [value: string] | — |
update:open | [value: boolean] | — |
focus | [event: FocusEvent] | — |
blur | [event: FocusEvent] | — |
Methods / Expose
| Methods / Expose | Type | Описание |
|---|---|---|
focus | () => void | — |
blur | () => void | — |
Примеры 2
Цвета бренда и подложек
Триггер показывает образец и текущее значение, панель — оттенок, насыщенность и светлоту тремя GrSlider, поле hex и палитру. alpha добавляет четвёртый канал и восьмизначную форму #RRGGBBAA; под прозрачным цветом видна шахматка.
#3b82f6#0f172acc<script setup lang="ts">
import { ref } from 'vue'
import { GrColorPicker } from '@feugene/granularity'
const brand = ref('#3b82f6')
const overlay = ref('#0f172acc')
const presets = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#0ea5e9', '#64748b', '#0f172a']
</script>
<template>
<div class="grid gap-4 sm:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]">
<div class="grid gap-3">
<div class="grid gap-1.5">
<span class="text-sm text-[var(--gr-muted-fg)]">Brand color</span>
<GrColorPicker v-model="brand" :presets="presets" aria-label="Brand color" />
</div>
<div class="grid gap-1.5">
<span class="text-sm text-[var(--gr-muted-fg)]">Overlay color</span>
<GrColorPicker v-model="overlay" alpha :presets="presets" aria-label="Overlay color" />
</div>
</div>
<div class="grid content-center gap-3 rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-5">
<span class="text-sm text-[var(--gr-muted-fg)]">Preview</span>
<div class="h-10 rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]" :style="{ background: brand }" />
<code class="text-xs text-[var(--gr-muted-fg)]">{{ brand }}</code>
<div class="h-10 rounded-[var(--gr-radius-md)] border border-[var(--gr-brd)]" :style="{ background: overlay }" />
<code class="text-xs text-[var(--gr-muted-fg)]">{{ overlay }}</code>
</div>
</div>
</template>Каналы сделаны слайдерами, а не двумерным квадратом, намеренно: каждый — настоящий role="slider" с полной клавиатурой и aria-valuetext («217°», «91 %»), тогда как квадрат пришлось бы озвучивать и водить с клавиатуры с нуля.
Внутри поля формы
Пикер — обычный форм-контрол: читает контекст GrFormField (подпись, подсказка, ошибка, disabled/readonly), участвует в правилах GrForm и отдаёт значение в нативную форму скрытым полем по пропу name.
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { GrButton, GrColorPicker, GrForm, GrFormField, GrInput, type GrFormRules } from '@feugene/granularity'
const model = reactive({ name: '', accent: '#22c55e' })
const rules: GrFormRules = {
name: [{ required: true }],
accent: [{ required: true, pattern: /^#[0-9a-f]{6}$/i, message: 'Only a six-digit hex is allowed' }],
}
const saved = ref('')
</script>
<template>
<GrForm :model="model" :rules="rules" class="grid max-w-sm gap-4" @submit="saved = model.accent">
<GrFormField name="name" label="Theme name">
<GrInput v-model="model.name" placeholder="Midnight" />
</GrFormField>
<GrFormField name="accent" label="Accent" hint="Goes to the --gr-primary token">
<GrColorPicker v-model="model.accent" name="accent" />
</GrFormField>
<GrButton type="submit" class="w-fit">
Save theme
</GrButton>
<p v-if="saved" class="text-sm text-[var(--gr-success-text)]">
Saved: {{ saved }}
</p>
</GrForm>
</template>Доступность
- Паттерн APG
dialog + slider- Клавиши
Enter/Spaceна триггере — открыть панель,Esc— закрыть и вернуть фокус на триггер,Tab— уйти из панели дальше по странице; внутри каналов — клавишиGrSlider