Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

ColorPicker

Выбор цвета хранит один цвет, а его части показывают и меняют его: ColorArea насыщенности и яркости, ColorSlider для любого канала RGB, HSL, HSB или OKLCH и альфа-канала, ColorField, которые принимают цвет как текст или один канал как число, ColorSwatch с готовыми цветами и ColorEyeDropper, выбирающий цвет в любом месте экрана. Приложение раскладывает части своими элементами между ними (строка на канал с его буквой, пространство, выбираемое с помощью Tabs, выбор цвета в Popover) и стилизует каждую из них: части рисуют цвета каналов, а все размеры, радиусы, границы и тени задаёт приложение.

el-color-picker.tsx
el-color-picker.tsxLive
import { mount, signal, Text, View } from "nsh";
import { type ColorChannel, ColorArea, ColorEyeDropper, ColorField, ColorPicker, ColorSlider, type ColorSpace, ColorSwatch } from "nsh/base";
import { desktop, field, Icon, Log, Segmented, Split, ui } from "./palette";

// `ColorPicker` holds the color; its parts, laid out and styled here, read
// and change it: the saturation and brightness area, the hue and alpha
// sliders, a row per channel of the space picked (its slider and its
// number), the color typed in any CSS form, the browser's eyedropper where
// it has one, and a palette of swatches.
const thumb = "rounded-full border-2 border-white";
const thumbStyle = { boxShadow: "0 0 0 1px rgba(0,0,0,0.25), 0 1px 3px rgba(0,0,0,0.3)" };
const spaces = ["rgb", "hsl", "hsb", "oklch"] as const;
const channels: Record<ColorSpace, [ColorChannel, string][]> = {
  rgb: [["red", "R"], ["green", "G"], ["blue", "B"]],
  hsl: [["hue", "H"], ["saturation", "S"], ["lightness", "L"]],
  hsb: [["hue", "H"], ["saturation", "S"], ["brightness", "B"]],
  oklch: [["lightness", "L"], ["chroma", "C"], ["hue", "H"]],
};
const palette = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899", "#0f172a80"];

/** A track `h` points high, half a thumb free at each end for the thumb. */
const track = (h: number) => ({
  class: `h-[${h + 8}px] justify-center px-[${(h + 6) / 2}px]`,
  trackClass: `h-[${h}px] rounded-full`,
  thumbClass: `w-[${h + 6}px] h-[${h + 6}px] ${thumb}`,
  thumbStyle,
  fillThumb: true,
});

function App() {
  const color = signal("#3b82f6ff");
  const text = signal("#3b82f6");
  const space = signal<ColorSpace>("oklch");
  const events = signal<string[]>([]);
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>color picker</Text>
      <Text style={{ color: ui().muted }}>
        {desktop ? "Drag the area or a slider, type a color, or pick a swatch" : "Drag the area or a slider, or tap a swatch"}
      </Text>
      <Split
        gap={14}
        inspector={
<Log title="onChangeEnd({ value, formatted })" lines={events()} minHeight={92} />
        }
      >
        <View style={{ ...ui().card, borderRadius: 16, padding: 16 }}>
          <ColorPicker
            value={color()}
            onChange={({ value, formatted }) => {
              color.set(value);
              text.set(formatted);
            }}
            onChangeEnd={({ formatted }) => events.set([formatted, ...events()].slice(0, 5))}
            style={{ gap: 12 }}
          >
            <ColorArea
              accessibilityLabel="Saturation and brightness"
              style={{ height: desktop ? 160 : 140 }}
              panelClass="rounded-lg"
              thumbClass={`w-[18px] h-[18px] ${thumb}`}
              thumbStyle={thumbStyle}
              fillThumb
            />
            <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
              <ColorSwatch accessibilityLabel="Current color" style={{ width: 36, height: 36, borderRadius: 18 }} checkerboardColor="#d4d4d8" />
              <View style={{ flex: 1, gap: 8 }}>
                <ColorSlider colorSpace="hsb" channel="hue" accessibilityLabel="Hue" {...track(12)} />
                <ColorSlider channel="alpha" accessibilityLabel="Alpha" checkerboardColor="#d4d4d8" {...track(12)} />
              </View>
            </View>
            <Text style={{ fontSize: 13, color: ui().muted, fontVariant: ["tabular-nums"] }}>{text()}</Text>
            <Segmented options={spaces} value={space()} onChange={(next) => space.set(next)} />
            {() =>
              channels[space()].map(([channel, letter]) => (
                <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
                  <Text style={{ width: 14, fontSize: 13, color: ui().muted }}>{letter}</Text>
                  <ColorSlider colorSpace={space()} channel={channel} accessibilityLabel={`${space()} ${channel}`} style={{ flex: 1 }} {...track(10)} />
                  <ColorField
                    colorSpace={space()}
                    channel={channel}
                    accessibilityLabel={`${space()} ${channel} value`}
                    {...field({ style: { width: 64, textAlign: "right" } })}
                  />
                </View>
              ))
            }
            <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
              <ColorField
                accessibilityLabel="Color"
                placeholder="#rrggbb, rgb(), hsl(), oklch() or a name"
                {...field({ style: { flex: 1 } })}
              />
              <ColorEyeDropper
                accessibilityLabel="Pick a color from the screen"
                class="disabled:hidden w-9 h-9 items-center justify-center rounded-md border border-border active:opacity-70"
              >
                <Icon name="pipette" size={16} tint={ui().text} />
              </ColorEyeDropper>
            </View>
            <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8 }}>
              {palette.map((c) => (
                <ColorSwatch
                  color={c}
                  style={{ width: 24, height: 24, borderRadius: 6 }}
                  selectedStyle={{ borderWidth: 2, borderColor: ui().text }}
                  checkerboardColor="#d4d4d8"
                  checkerboardSize={6}
                />
              ))}
            </View>
          </ColorPicker>
        </View>
      </Split>
    </View>
  );
}

mount(App);
use nsh_base::{
    ColorChannel, ColorPickerEvent, ColorSpace, color_area, color_field, color_picker,
    color_slider, color_swatch, theme,
};
use nsh_core::prelude::*;

fn primary(color: Signal<Hsla>) -> impl IntoElement {
    // A channel of OKLCH: its letter, its slider, its number.
    let channel = move |channel: ColorChannel, letter: &'static str| {
        view()
            .flex_row()
            .items_center()
            .gap_2()
            .child(text(letter).w(px(14.0)).text_sm().text_color(move || theme().muted_foreground))
            .child(
                color_slider()
                    .color_space(ColorSpace::Oklch)
                    .channel(channel)
                    .accessibility_label(letter)
                    .flex_1()
                    .h(px(18.0))
                    .justify_center()
                    .px(px(8.0))
                    .track_style(|s| s.h(px(10.0)).rounded_full())
                    .thumb_style(|s| s.size(px(16.0)).rounded_full().border_2().border_color(white()))
                    .fill_thumb(true),
            )
            .child(
                color_field()
                    .color_space(ColorSpace::Oklch)
                    .channel(channel)
                    .accessibility_label(letter)
                    .w(px(64.0))
                    .h(px(28.0))
                    .px_2()
                    .rounded_md()
                    .border_1()
                    .border_color(move || theme().border)
                    .text_sm(),
            )
    };
    color_picker()
        .value(move || Some(color.get()))
        .on_change(move |e: &ColorPickerEvent| {
            if let ColorPickerEvent::Change(value, _) = e {
                color.set(*value);
            }
        })
        .gap_3()
        .child(
            color_area()
                .accessibility_label("Saturation and brightness")
                .h(px(160.0))
                .panel_style(|s| s.rounded_lg())
                .thumb_style(|s| s.size(px(18.0)).rounded_full().border_2().border_color(white()))
                .fill_thumb(true),
        )
        .child(channel(ColorChannel::Lightness, "L"))
        .child(channel(ColorChannel::Chroma, "C"))
        .child(channel(ColorChannel::Hue, "H"))
        .child(color_swatch().size(px(28.0)).rounded_md())
}

ColorPicker, ColorArea, ColorSlider, ColorField, ColorSwatch и ColorEyeDropper поставляются из nsh/base, крейта nsh-base в Rust. В Rust color_picker() и функции его частей создают их с помощью сеттера на каждое свойство (.channel(..), .thumb_style(..)), метода-слушателя на каждое событие (.on_change(..)) и типизированных методов стиля; цвет — это Hsla, пространства, каналы и форматы — ColorSpace, ColorChannel и ColorFormat.

#Значение

Цвет — это CSS-цвет: hex (#3b82f6, #3b82f680 с альфа-каналом), rgb(), hsl(), hsb(), oklch() или CSS-имя; в Rust — Hsla. Приложение владеет им через value и устанавливает тот цвет, о котором сообщает onChange, как для каждого элемента управления nsh/base; либо оставляет цвет выбору с помощью defaultValue и всё равно получает уведомления об изменениях.

  • onChange({ value, formatted }) сообщает о каждом цвете, который запрашивает пользователь: на каждом шаге перетаскивания по области или слайдеру, при каждом нажатии клавиши, при вводе числа в поле, при нажатии на образец. value — это цвет, formatted — тот же цвет, записанный так, как указывает format ("hex", "rgb", "hsl", "hsb" или "oklch"), готовый к сохранению или к использованию как токен.
  • onChangeEnd({ value, formatted }) один раз сообщает цвет, на котором закончилось изменение: когда указатель отпущен, после каждой клавиши, когда поле покинуто или отправлено, когда нажат образец.
  • Выбор цвета хранит цвет в каждом пространстве в том виде, как его задал пользователь. Серый сохраняет оттенок и насыщенность, поэтому слайдер оттенка остаётся там, где его оставил пользователь; цвет, изменённый в OKLCH, сохраняет светлоту, chroma и оттенок так, как они введены, в том числе за пределами того, что показывает sRGB (сообщаемый цвет — ближайший из тех, что показывает sRGB, а formatted в "oklch" сохраняет каналы).
  • value, заданный приложением и не сообщённый выбором цвета, заменяет цвет во всех пространствах.

#Пространства и каналы

colorSpaceКаналы (channel)Диапазоны
"rgb""red", "green", "blue"от 0 до 255
"hsl""hue", "saturation", "lightness"оттенок от 0 до 360, остальные от 0 до 100
"hsb""hue", "saturation", "brightness"оттенок от 0 до 360, остальные от 0 до 100
"oklch""lightness", "chroma", "hue"светлота от 0 до 100 (шаг 0.1), chroma от 0 до 0.4 (шаг 0.001), оттенок от 0 до 360 (шаг 0.1)
любое"alpha"от 0 до 100

Трек слайдера рисует цвета своего канала при неизменных остальных каналах, поэтому показывает, к чему приводит каждое значение. Канал, которого нет в его пространстве, заменяется первым каналом пространства (об этом сообщает предупреждение).

#Свойства

#ColorPicker

СвойствоRustТипПо умолчаниюОписание
value.value(..)stringнетЦвет (управляемый). Приложение устанавливает тот цвет, о котором сообщает onChange.
defaultValue.default_value(..)string"#000000"Начальный цвет, пока value не задан; тогда выбор цвета хранит собственный цвет.
format.format(..)"hex" | "rgb" | "hsl" | "hsb" | "oklch""hex"Как formatted записывает цвет.
disabled.disabled(..)booleanfalseНикаких изменений ни от одной части; классы disabled: применяются к выбору цвета и его частям.
children.child(..)JSX.ElementнетЧасти и элементы приложения между ними.
class, styleметоды стиляstring, StyleProp<ViewStyle>нетСам выбор цвета: колонка (или строка) вокруг его частей.

#ColorArea

СвойствоRustТипПо умолчаниюОписание
panelClass, panelStyle.panel_style(|s| ..)string, StyleProp<ViewStyle>нетПанель, заполняющая область: цвета оттенка, насыщенность слева направо (справа налево в раскладке справа налево) и яркость снизу вверх, обрезанные по её углам.
thumbClass, thumbStyle.thumb_style(|s| ..)string, StyleProp<ViewStyle>нетБегунок, центрированный на цвете.
fillThumb.fill_thumb(..)booleanfalseФон бегунка — это цвет.
disabled.disabled(..)booleanfalseНикаких изменений от указателя, клавиатуры или средств чтения с экрана.
accessibilityLabel.accessibility_label(..)stringнетЧто средства чтения с экрана говорят о бегунке.
class, styleметоды стиляstring, StyleProp<ViewStyle>нетОбласть: её размер задаёт приложение.

#ColorSlider

СвойствоRustТипПо умолчаниюОписание
colorSpace.color_space(..)"rgb" | "hsl" | "hsb" | "oklch""hsb"Пространство канала.
channel.channel(..)ColorChannel"hue"Канал, который он меняет: один из каналов пространства или "alpha".
axis.axis(..)"horizontal" | "vertical""horizontal"Направление трека: наименьшее значение в начале (слева, справа в раскладке справа налево) или внизу. Считывается при создании слайдера.
trackClass, trackStyle.track_style(|s| ..)string, StyleProp<ViewStyle>нетТрек: цвета канала вдоль него, обрезанные по его углам; бегунок размещается на нём.
thumbClass, thumbStyle.thumb_style(|s| ..)string, StyleProp<ViewStyle>нетБегунок, центрированный на значении.
fillThumb.fill_thumb(..)booleanfalseФон бегунка — это цвет (с его альфа-каналом на слайдере альфа-канала).
checkerboardColor.checkerboard_color(..)stringнетЦвет шахматного фона под треком, который просвечивает сквозь цвета слайдера альфа-канала.
checkerboardSize.checkerboard_size(..)number4Сторона квадратов шахматного фона.
disabled.disabled(..)booleanfalseНикаких изменений от указателя, клавиатуры или средств чтения с экрана.
accessibilityLabel.accessibility_label(..)stringнетЧто средства чтения с экрана говорят о бегунке.
class, styleметоды стиляstring, StyleProp<ViewStyle>нетСлайдер: полоса вокруг трека, принимающая нажатия и перетаскивания.

#ColorField

СвойствоRustТипПо умолчаниюОписание
channel.channel(..)ColorChannelнетКанал, который поле принимает как число. Без него поле принимает весь цвет как текст.
colorSpace.color_space(..)ColorSpace"hsb"Пространство channel.
format.format(..)ColorFormat"hex"Как поле записывает весь цвет. Читает любую форму CSS.
placeholder.placeholder(..)stringнетПоказывается, пока поле пусто.
disabled.disabled(..)booleanfalseВвод невозможен.
accessibilityLabel.accessibility_label(..)stringнетЧто средства чтения с экрана говорят о поле.
class, styleметоды стиляstring, StyleProp<TextStyle>нетПоле, текстовый ввод.

Поле канала меняет цвет по мере ввода числа, ограничивая его диапазоном канала; поле всего цвета меняет его, когда поле покинуто или отправлено, если текст читается как цвет. При выходе из поля его значение записывается заново в своей форме.

#ColorSwatch

СвойствоRustТипПо умолчаниюОписание
color.color(..)stringнетЦвет, который он показывает; нажатие выбирает его в окружающем выборе цвета. Без него — собственный цвет выбора: предпросмотр.
selectedStyle.selected_style(..)StyleProp<ViewStyle>нетСтиль, пока это цвет выбора, в виде классов selected:.
checkerboardColor.checkerboard_color(..)stringнетШахматный фон под полупрозрачным цветом.
checkerboardSize.checkerboard_size(..)number4Сторона его квадратов.
disabled.disabled(..)booleanfalseНажатия не обрабатываются.
accessibilityLabel.accessibility_label(..)stringцвет в hexЧто говорят средства чтения с экрана.
children.child(..)JSX.ElementнетСодержимое поверх цвета: галочка на выбранном образце (group-selected:).
class, styleметоды стиляstring, StyleProp<ViewStyle>нетОбразец: его размер, форма и граница; его углы обрезают цвет.

#ColorEyeDropper

СвойствоRustТипПо умолчаниюОписание
disabled.disabled(..)booleanfalseНажатия не обрабатываются. Кнопка также отключена там, где на платформе нет пипетки.
accessibilityLabel.accessibility_label(..)stringнетЧто говорят средства чтения с экрана.
children.child(..)JSX.ElementнетЕё содержимое: значок или подпись приложения.
class, styleметоды стиляstring, StyleProp<ViewStyle>нетКнопка.

Нажатие показывает системную пипетку: color sampler из AppKit на macOS, EyeDropper браузера в вебе (Chrome и Edge). Выбранный цвет становится цветом выбора (onChange, затем onChangeEnd); Escape не выбирает ничего. Там, где на платформе нет пипетки, кнопка отключена, а disabled:hidden скрывает её.

#События

СобытиеЧьёRustДанныеКогда
onChangeColorPicker.on_change(|e: &ColorPickerEvent| ..)({ value: string, formatted: string })Пользователь изменил цвет.
onChangeEndColorPicker.on_change_end(|e: &ColorPickerEvent| ..)({ value: string, formatted: string })Изменение завершено. Один раз на изменение.
onPressColorSwatch.on_press(|color: &Hsla| ..)({ color: string })Образец нажат; работает и отдельно, как элемент палитры.
onPickColorEyeDropper.on_pick(|color: &Hsla| ..)({ color: string })Пользователь выбрал цвет на экране; работает и отдельно.

В Rust ColorPickerEvent::Change(Hsla, SharedString) и ColorPickerEvent::ChangeEnd(Hsla, SharedString) несут цвет и его текст formatted.

#Части

ЧастьВнутриСтилизуется через
ColorAreaвыбор цветаclass, style
Панель областиобласть, заполняет еёpanelClass, panelStyle
Бегунок областиобласть, по центру на цветеthumbClass, thumbStyle
ColorSliderвыбор цветаclass, style
Трек слайдераслайдерtrackClass, trackStyle
Бегунок слайдераслайдер, по центру на значении на трекеthumbClass, thumbStyle
ColorFieldвыбор цветаclass, style
ColorSwatchвыбор цвета или отдельноclass, style, selectedStyle
ColorEyeDropperвыбор цвета или отдельноclass, style

Бегунок слайдера центрируется на концах трека при крайних значениях канала. Внутренний отступ слайдера, равный половине ширины бегунка (h-6 justify-center px-2.5 вокруг трека h-3), удерживает бегунок целиком внутри слайдера, где он принимает нажатия на обоих концах. Состояния, которые показывают части: active: бегунка при перетаскивании и focus: при фокусе клавиатуры, selected: образца и disabled: каждой части.

#Клавиатура

На macOS и в вебе каждый бегунок — остановка фокуса клавиатуры (Tab):

КлавишаНа слайдереНа области
Вправо, ВверхОдин шаг вверхВправо: больше насыщенности; Вверх: больше яркости
Влево, ВнизОдин шаг внизВлево: меньше насыщенности; Вниз: меньше яркости
Shift со стрелкойДесять шаговДесять шагов
Page Up, Page DownДесять шаговДесять шагов яркости
Home, EndКонцы канала

В раскладке справа налево горизонтальный слайдер и область идут справа: наименьшее значение (наименьшая насыщенность) находится на правом конце, Влево — это шаг вверх, а Вправо — шаг вниз, а нажатие или перетаскивание выбирает значение под указателем так, как оно нарисовано. Вертикальный слайдер сохраняет Вправо и Вверх как шаг вверх; Вверх и Вниз на области сохраняют яркость.

Образец принимает Space и Return. Каждая клавиша сообщает onChange, а затем onChangeEnd; стрелки повторяются при удержании.

#Доступность

  • Бегунки области и каждого слайдера — регулируемые элементы ("adjustable", слайдер в браузере) со своей меткой, значением и диапазоном; увеличение и уменьшение в средствах чтения с экрана двигают их по шагам.
  • Каждое поле — текстовое поле со своей меткой; каждый образец — кнопка, выбранная, пока это цвет выбора, с именем по цвету в hex, если имя не задаёт accessibilityLabel. Пипетка — кнопка с меткой приложения.
  • В отключённом состоянии средства чтения с экрана сообщают, что части затемнены, и они не принимают действий.

Подробнее о метках, ролях и действиях в целом см. в разделе Доступность.

#Примечания по платформам

  • Каждая платформа рисует то, что стилизует приложение; цвета треков и панели — это градиенты, рисуемые платформой. Нажатия и перетаскивания отслеживаются в ядре для мыши, пера или пальца, а прокручиваемое представление вокруг выбора цвета сохраняет прокрутку для перетаскиваний, начатых в другом месте.
  • ColorEyeDropper выбирает цвет с помощью color sampler из AppKit на macOS и EyeDropper браузера в вебе; цвет приходит в sRGB.