Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Elementos

ColorPicker

Um seletor de cor guarda uma cor, e as partes dentro dele a mostram e a alteram: uma ColorArea de saturação e brilho, ColorSliders de qualquer canal de RGB, HSL, HSB ou OKLCH e do alfa, ColorFields que recebem a cor como texto ou um canal como número, ColorSwatchs de cores predefinidas e um ColorEyeDropper que escolhe uma cor em qualquer lugar da tela. O app organiza as partes com seus próprios elementos entre elas (uma linha por canal com sua letra, o espaço escolhido com Tabs, o seletor em um Popover), e estiliza cada uma: as partes desenham as cores dos canais, e todo tamanho, raio, borda e sombra é do app.

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 e ColorEyeDropper vêm de nsh/base, o crate nsh-base em Rust. Em Rust, color_picker() e as funções de suas partes os criam com um setter por prop (.channel(..), .thumb_style(..)), um método de listener por evento (.on_change(..)) e os métodos de estilo tipados; a cor é um Hsla, os espaços, canais e formatos são ColorSpace, ColorChannel e ColorFormat.

#Valor

A cor é uma cor CSS: hex (#3b82f6, #3b82f680 com seu alfa), rgb(), hsl(), hsb(), oklch() ou um nome CSS; em Rust, um Hsla. O app a controla com value e define a que onChange reporta, como em todo controle de nsh/base; ou deixa a cor com o seletor usando defaultValue e ouve as mudanças do mesmo jeito.

  • onChange({ value, formatted }) reporta cada cor que o usuário pede: a cada passo de um arrasto na área ou em um slider, a cada tecla, o número de um campo conforme é digitado, uma amostra pressionada. value é a cor, formatted a mesma cor escrita como format determina ("hex", "rgb", "hsl", "hsb" ou "oklch"), pronta para salvar ou para definir como token.
  • onChangeEnd({ value, formatted }) reporta a cor em que a mudança terminou, uma única vez: quando o ponteiro é solto, após cada tecla, quando um campo perde o foco ou é enviado, quando uma amostra é pressionada.
  • O seletor guarda a cor em cada espaço como o usuário a definiu. Um cinza mantém seu matiz e sua saturação, então o slider de matiz permanece onde o usuário o deixou; uma cor editada em OKLCH mantém sua luminosidade, croma e matiz como digitados, inclusive além do que o sRGB mostra (a cor reportada é a mais próxima que o sRGB mostra, e formatted em "oklch" mantém os canais).
  • Um value definido pelo app que o seletor não reportou substitui a cor em todos os espaços.

#Espaços e canais

colorSpace Canais (channel) Intervalos
"rgb" "red", "green", "blue" 0 a 255
"hsl" "hue", "saturation", "lightness" matiz 0 a 360, os demais 0 a 100
"hsb" "hue", "saturation", "brightness" matiz 0 a 360, os demais 0 a 100
"oklch" "lightness", "chroma", "hue" luminosidade 0 a 100 (de 0,1 em 0,1), croma 0 a 0,4 (de 0,001 em 0,001), matiz 0 a 360 (de 0,1 em 0,1)
qualquer "alpha" 0 a 100

O trilho de um slider desenha as cores do seu canal mantendo os outros canais fixos, de modo que mostra para onde cada valor leva. Um canal que o seu espaço não tem vira o primeiro canal do espaço (um aviso informa isso).

#Props

#ColorPicker

Prop Rust Tipo Padrão Descrição
value .value(..) string nenhum A cor (controlada). O app define a que onChange reporta.
defaultValue .default_value(..) string "#000000" A cor inicial enquanto value não estiver definido; o seletor então mantém a sua própria.
format .format(..) "hex" | "rgb" | "hsl" | "hsb" | "oklch" "hex" Como formatted escreve a cor.
disabled .disabled(..) boolean false Nenhuma mudança vinda de qualquer parte; as classes disabled: se aplicam ao seletor e às suas partes.
children .child(..) JSX.Element nenhum As partes e os elementos do app entre elas.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum O próprio seletor: a coluna (ou linha) em volta de suas partes.

#ColorArea

Prop Rust Tipo Padrão Descrição
panelClass, panelStyle .panel_style(|s| ..) string, StyleProp<ViewStyle> nenhum O painel, preenchendo a área: as cores do matiz, com a saturação da esquerda para a direita (da direita para a esquerda em um layout da direita para a esquerda) e o brilho de baixo para cima, recortado nos seus cantos.
thumbClass, thumbStyle .thumb_style(|s| ..) string, StyleProp<ViewStyle> nenhum O thumb, centralizado na cor.
fillThumb .fill_thumb(..) boolean false O fundo do thumb é a cor.
disabled .disabled(..) boolean false Nenhuma mudança por ponteiro, teclado ou leitor de tela.
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem para o thumb.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum A área: seu tamanho é do app.

#ColorSlider

Prop Rust Tipo Padrão Descrição
colorSpace .color_space(..) "rgb" | "hsl" | "hsb" | "oklch" "hsb" O espaço do canal.
channel .channel(..) ColorChannel "hue" O canal que ele altera: um dos canais do espaço, ou "alpha".
axis .axis(..) "horizontal" | "vertical" "horizontal" A direção do trilho: o menor valor no início (a esquerda, ou a direita em um layout da direita para a esquerda), ou embaixo. Lida quando o slider é criado.
trackClass, trackStyle .track_style(|s| ..) string, StyleProp<ViewStyle> nenhum O trilho: as cores do canal ao longo dele, recortadas nos seus cantos; o thumb é posicionado sobre ele.
thumbClass, thumbStyle .thumb_style(|s| ..) string, StyleProp<ViewStyle> nenhum O thumb, centralizado no valor.
fillThumb .fill_thumb(..) boolean false O fundo do thumb é a cor (com seu alfa em um slider de alfa).
checkerboardColor .checkerboard_color(..) string nenhum A cor de um padrão xadrez sob o trilho, que aparece através das cores de um slider de alfa.
checkerboardSize .checkerboard_size(..) number 4 O lado dos quadrados do xadrez.
disabled .disabled(..) boolean false Nenhuma mudança por ponteiro, teclado ou leitor de tela.
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem para o thumb.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum O slider: a faixa em volta do trilho que recebe toques e arrastos.

#ColorField

Prop Rust Tipo Padrão Descrição
channel .channel(..) ColorChannel nenhum O canal que ele recebe como número. Sem ele, o campo recebe a cor inteira como texto.
colorSpace .color_space(..) ColorSpace "hsb" O espaço de channel.
format .format(..) ColorFormat "hex" Como o campo escreve a cor inteira. Ele lê qualquer forma CSS.
placeholder .placeholder(..) string nenhum Exibido enquanto o campo está vazio.
disabled .disabled(..) boolean false Sem digitação.
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem para o campo.
class, style métodos de estilo string, StyleProp<TextStyle> nenhum O campo, uma entrada de texto.

O campo de um canal altera a cor conforme um número é digitado, limitado ao intervalo do canal; o campo da cor inteira a altera quando o campo perde o foco ou é enviado, se o texto puder ser lido como uma cor. Sair de um campo reescreve seu valor na sua forma.

#ColorSwatch

Prop Rust Tipo Padrão Descrição
color .color(..) string nenhum A cor que ela mostra; um toque a escolhe no seletor em volta. Sem ela, a própria cor do seletor: uma pré-visualização.
selectedStyle .selected_style(..) StyleProp<ViewStyle> nenhum O estilo enquanto ela é a cor do seletor, como classes selected:.
checkerboardColor .checkerboard_color(..) string nenhum Um padrão xadrez sob uma cor translúcida.
checkerboardSize .checkerboard_size(..) number 4 O lado dos seus quadrados.
disabled .disabled(..) boolean false Sem toques.
accessibilityLabel .accessibility_label(..) string a cor em hex O que os leitores de tela dizem.
children .child(..) JSX.Element nenhum Conteúdo sobre a cor: um sinal de seleção na amostra selecionada (group-selected:).
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum A amostra: seu tamanho, forma e borda; seus cantos recortam a cor.

#ColorEyeDropper

Prop Rust Tipo Padrão Descrição
disabled .disabled(..) boolean false Sem toques. Também fica desabilitado onde a plataforma não tem conta-gotas.
accessibilityLabel .accessibility_label(..) string nenhum O que os leitores de tela dizem.
children .child(..) JSX.Element nenhum Seu conteúdo: o ícone ou o rótulo do app.
class, style métodos de estilo string, StyleProp<ViewStyle> nenhum O botão.

Um toque mostra o conta-gotas do sistema: o color sampler do AppKit no macOS, o EyeDropper do navegador na web (Chrome e Edge). A cor escolhida passa a ser a do seletor (onChange, depois onChangeEnd); Escape não escolhe nenhuma. Onde a plataforma não tem conta-gotas, o botão fica desabilitado, e disabled:hidden o oculta ali.

#Eventos

Evento De Rust Payload Quando
onChange ColorPicker .on_change(|e: &ColorPickerEvent| ..) ({ value: string, formatted: string }) O usuário alterou a cor.
onChangeEnd ColorPicker .on_change_end(|e: &ColorPickerEvent| ..) ({ value: string, formatted: string }) A mudança terminou. Uma vez por mudança.
onPress ColorSwatch .on_press(|color: &Hsla| ..) ({ color: string }) A amostra foi pressionada; também funciona sozinha, como o chip de uma paleta.
onPick ColorEyeDropper .on_pick(|color: &Hsla| ..) ({ color: string }) O usuário escolheu uma cor na tela; também funciona sozinho.

Em Rust, ColorPickerEvent::Change(Hsla, SharedString) e ColorPickerEvent::ChangeEnd(Hsla, SharedString) carregam a cor e o seu texto formatted.

#Partes

Parte Dentro de Estilizada com
ColorArea o seletor class, style
O painel da área a área, preenchendo-a panelClass, panelStyle
O thumb da área a área, centralizado na cor thumbClass, thumbStyle
ColorSlider o seletor class, style
O trilho do slider o slider trackClass, trackStyle
O thumb do slider o slider, centralizado no valor sobre o trilho thumbClass, thumbStyle
ColorField o seletor class, style
ColorSwatch o seletor, ou sozinha class, style, selectedStyle
ColorEyeDropper o seletor, ou sozinho class, style

O thumb de um slider é centralizado nas extremidades do trilho nas extremidades do canal. Um padding no slider de metade da largura do thumb (h-6 justify-center px-2.5 em volta de um trilho h-3) mantém o thumb inteiro dentro do slider, onde ele recebe toques nas duas extremidades. Estados que as partes mostram: o active: de um thumb enquanto ele é arrastado e o focus: com o foco do teclado, o selected: de uma amostra, e o disabled: de toda parte.

#Teclado

No macOS e na web, cada thumb é uma parada do foco do teclado (Tab):

Tecla Em um slider Na área
Right, Up Um passo para cima Right: mais saturação; Up: mais brilho
Left, Down Um passo para baixo Left: menos saturação; Down: menos brilho
Shift com uma seta Dez passos Dez passos
Page Up, Page Down Dez passos Dez passos de brilho
Home, End As extremidades do canal

Em um layout da direita para a esquerda, um slider horizontal e a área correm a partir da direita, com o menor valor (a menor saturação) na extremidade direita: Left é um passo para cima e Right um passo para baixo, e um toque ou um arrasto escolhe o valor sob o ponteiro como ele é desenhado. Um slider vertical mantém Right e Up como um passo para cima; Up e Down na área mantêm o brilho.

Uma amostra aceita Space e Return. Cada tecla reporta onChange e depois onChangeEnd; as setas se repetem enquanto pressionadas.

#Acessibilidade

  • O thumb da área e o de cada slider são elementos ajustáveis ("adjustable", um slider no navegador) com seu rótulo, valor e intervalo; o incremento e o decremento dos leitores de tela os movem passo a passo.
  • Cada campo é um campo de texto com seu rótulo; cada amostra um botão, selecionado enquanto é a cor do seletor, nomeado por sua cor em hex, a menos que accessibilityLabel a nomeie. O conta-gotas é um botão com o rótulo do app.
  • Enquanto desabilitadas, os leitores de tela dizem que as partes estão esmaecidas, e elas não recebem nenhuma ação.

Veja Acessibilidade para rótulos, papéis e ações em geral.

#Notas de plataforma

  • Toda plataforma desenha o que o app estiliza; as cores dos trilhos e do painel são gradientes desenhados pela plataforma. Toques e arrastos são acompanhados no core, para um mouse, uma caneta ou um dedo, e uma scroll view em volta do seletor mantém sua rolagem para arrastos que começam em outro lugar.
  • ColorEyeDropper escolhe com o color sampler do AppKit no macOS e com o EyeDropper do navegador na web; a cor chega em sRGB.