Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

Elementi

ColorPicker

Un color picker contiene un colore, e le parti al suo interno lo mostrano e lo modificano: un ColorArea di saturazione e luminosità, ColorSlider per qualsiasi canale di RGB, HSL, HSB o OKLCH e per l’alpha, ColorField che accettano il colore come testo o un canale come numero, ColorSwatch di colori predefiniti e un ColorEyeDropper che preleva un colore ovunque sullo schermo. L’app dispone le parti con i propri elementi frapposti (una riga per canale con la sua lettera, lo spazio scelto con Tabs, il picker in un Popover) e ne definisce lo stile: le parti disegnano i colori dei canali, mentre ogni dimensione, raggio, bordo e ombra è a cura dell’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 provengono da nsh/base, il crate nsh-base in Rust. In Rust, color_picker() e le funzioni delle sue parti li creano con un setter per ogni prop (.channel(..), .thumb_style(..)), un metodo listener per ogni evento (.on_change(..)) e i metodi di stile tipizzati; il colore è un Hsla, mentre spazi, canali e formati sono ColorSpace, ColorChannel e ColorFormat.

#Valore

Il colore è un colore CSS: esadecimale (#3b82f6, #3b82f680 con il suo alpha), rgb(), hsl(), hsb(), oklch() o un nome CSS; in Rust un Hsla. L’app lo gestisce con value e imposta quello che onChange segnala, come per ogni controllo di nsh/base; oppure lascia il colore al picker con defaultValue e ascolta comunque le modifiche.

  • onChange({ value, formatted }) segnala ogni colore richiesto dall’utente: a ogni passo di un trascinamento sull’area o su uno slider, a ogni tasto, il numero di un campo mentre viene digitato, una campione premuto. value è il colore, formatted lo stesso colore scritto come indicato da format ("hex", "rgb", "hsl", "hsb" o "oklch"), pronto per essere salvato o impostato come token.
  • onChangeEnd({ value, formatted }) segnala il colore a cui la modifica è terminata, una sola volta: quando il puntatore si solleva, dopo ogni tasto, quando si esce da un campo o lo si invia, quando si preme un campione.
  • Il picker conserva il colore in ogni spazio così come l’utente lo ha impostato. Un grigio mantiene la sua tonalità e saturazione, così lo slider della tonalità resta dove l’utente lo ha lasciato; un colore modificato in OKLCH mantiene luminosità, croma e tonalità come digitati, anche oltre ciò che sRGB mostra (il colore segnalato è il più vicino tra quelli che sRGB mostra, e formatted in "oklch" conserva i canali).
  • Un value impostato dall’app e non segnalato dal picker sostituisce il colore in ogni spazio.

#Spazi e canali

colorSpaceCanali (channel)Intervalli
"rgb""red", "green", "blue"da 0 a 255
"hsl""hue", "saturation", "lightness"tonalità da 0 a 360, gli altri da 0 a 100
"hsb""hue", "saturation", "brightness"tonalità da 0 a 360, gli altri da 0 a 100
"oklch""lightness", "chroma", "hue"luminosità da 0 a 100 (a passi di 0,1), croma da 0 a 0,4 (a passi di 0,001), tonalità da 0 a 360 (a passi di 0,1)
qualsiasi"alpha"da 0 a 100

La traccia di uno slider disegna i colori del suo canale tenendo fermi gli altri canali, così mostra dove porta ogni valore. Un canale che il suo spazio non possiede viene sostituito dal primo canale dello spazio (un avviso lo segnala).

#Props

#ColorPicker

PropRustTipoPredefinitoDescrizione
value.value(..)stringnessunoIl colore (controllato). L’app imposta quello che onChange segnala.
defaultValue.default_value(..)string"#000000"Il colore iniziale finché value non è impostato; il picker poi mantiene il proprio.
format.format(..)"hex" | "rgb" | "hsl" | "hsb" | "oklch""hex"Come formatted scrive il colore.
disabled.disabled(..)booleanfalseNessuna modifica da alcuna parte; le classi disabled: si applicano al picker e alle sue parti.
children.child(..)JSX.ElementnessunoLe parti e gli elementi dell’app tra di esse.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl picker stesso: la colonna (o riga) attorno alle sue parti.

#ColorArea

PropRustTipoPredefinitoDescrizione
panelClass, panelStyle.panel_style(|s| ..)string, StyleProp<ViewStyle>nessunoIl pannello, che riempie l’area: i colori della tonalità, la saturazione da sinistra a destra (da destra a sinistra in un layout da destra a sinistra) e la luminosità dal basso verso l’alto, ritagliati agli angoli.
thumbClass, thumbStyle.thumb_style(|s| ..)string, StyleProp<ViewStyle>nessunoIl thumb, centrato sul colore.
fillThumb.fill_thumb(..)booleanfalseLo sfondo del thumb è il colore.
disabled.disabled(..)booleanfalseNessuna modifica da puntatore, tastiera o screen reader.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che gli screen reader dicono per il thumb.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoL’area: la sua dimensione spetta all’app.

#ColorSlider

PropRustTipoPredefinitoDescrizione
colorSpace.color_space(..)"rgb" | "hsl" | "hsb" | "oklch""hsb"Lo spazio del canale.
channel.channel(..)ColorChannel"hue"Il canale che modifica: uno di quelli dello spazio, oppure "alpha".
axis.axis(..)"horizontal" | "vertical""horizontal"La direzione della traccia: il valore minimo all’inizio (a sinistra, a destra in un layout da destra a sinistra) o in basso. Letta quando lo slider viene creato.
trackClass, trackStyle.track_style(|s| ..)string, StyleProp<ViewStyle>nessunoLa traccia: i colori del canale lungo di essa, ritagliati agli angoli; il thumb è posizionato su di essa.
thumbClass, thumbStyle.thumb_style(|s| ..)string, StyleProp<ViewStyle>nessunoIl thumb, centrato sul valore.
fillThumb.fill_thumb(..)booleanfalseLo sfondo del thumb è il colore (con il suo alpha in uno slider alpha).
checkerboardColor.checkerboard_color(..)stringnessunoIl colore di una scacchiera sotto la traccia, che traspare attraverso i colori di uno slider alpha.
checkerboardSize.checkerboard_size(..)number4Il lato dei quadrati della scacchiera.
disabled.disabled(..)booleanfalseNessuna modifica da puntatore, tastiera o screen reader.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che gli screen reader dicono per il thumb.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoLo slider: la fascia attorno alla traccia che riceve pressioni e trascinamenti.

#ColorField

PropRustTipoPredefinitoDescrizione
channel.channel(..)ColorChannelnessunoIl canale che accetta come numero. Senza di esso, il campo accetta l’intero colore come testo.
colorSpace.color_space(..)ColorSpace"hsb"Lo spazio di channel.
format.format(..)ColorFormat"hex"Come il campo scrive l’intero colore. Legge qualsiasi forma CSS.
placeholder.placeholder(..)stringnessunoMostrato mentre il campo è vuoto.
disabled.disabled(..)booleanfalseNessuna digitazione.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che gli screen reader dicono per il campo.
class, stylemetodi di stilestring, StyleProp<TextStyle>nessunoIl campo, un input di testo.

Il campo di un canale modifica il colore mentre si digita un numero, limitato all’intervallo del canale; il campo dell’intero colore lo modifica quando si esce dal campo o lo si invia, se il testo è leggibile come colore. Uscendo da un campo, il suo valore viene riscritto nella sua forma.

#ColorSwatch

PropRustTipoPredefinitoDescrizione
color.color(..)stringnessunoIl colore che mostra; una pressione lo sceglie nel picker che lo contiene. Senza di esso, il colore del picker stesso: un’anteprima.
selectedStyle.selected_style(..)StyleProp<ViewStyle>nessunoLo stile mentre è il colore del picker, come classi selected:.
checkerboardColor.checkerboard_color(..)stringnessunoUna scacchiera sotto un colore traslucido.
checkerboardSize.checkerboard_size(..)number4Il lato dei suoi quadrati.
disabled.disabled(..)booleanfalseNessuna pressione.
accessibilityLabel.accessibility_label(..)stringil colore in esadecimaleCiò che gli screen reader dicono.
children.child(..)JSX.ElementnessunoContenuto sopra il colore: un segno di spunta sul campione selezionato (group-selected:).
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl campione: dimensione, forma e bordo; i suoi angoli ritagliano il colore.

#ColorEyeDropper

PropRustTipoPredefinitoDescrizione
disabled.disabled(..)booleanfalseNessuna pressione. È disabilitato anche dove la piattaforma non ha un contagocce.
accessibilityLabel.accessibility_label(..)stringnessunoCiò che gli screen reader dicono.
children.child(..)JSX.ElementnessunoIl suo contenuto: l’icona o l’etichetta dell’app.
class, stylemetodi di stilestring, StyleProp<ViewStyle>nessunoIl pulsante.

Una pressione mostra il contagocce del sistema: il color sampler di AppKit su macOS, EyeDropper del browser sul web (Chrome ed Edge). Il colore prelevato diventa quello del picker (onChange, poi onChangeEnd); Escape non preleva nulla. Dove la piattaforma non ha un contagocce il pulsante è disabilitato, e disabled:hidden lo nasconde in quel caso.

#Eventi

EventoDiRustPayloadQuando
onChangeColorPicker.on_change(|e: &ColorPickerEvent| ..)({ value: string, formatted: string })L’utente ha modificato il colore.
onChangeEndColorPicker.on_change_end(|e: &ColorPickerEvent| ..)({ value: string, formatted: string })La modifica è terminata. Una volta per modifica.
onPressColorSwatch.on_press(|color: &Hsla| ..)({ color: string })Il campione è stato premuto; funziona anche da solo, come chip di una palette.
onPickColorEyeDropper.on_pick(|color: &Hsla| ..)({ color: string })L’utente ha prelevato un colore sullo schermo; funziona anche da solo.

In Rust, ColorPickerEvent::Change(Hsla, SharedString) e ColorPickerEvent::ChangeEnd(Hsla, SharedString) contengono il colore e il suo testo formatted.

#Parti

ParteDentroStile con
ColorAreail pickerclass, style
Il pannello dell’areal’area, che riempiepanelClass, panelStyle
Il thumb dell’areal’area, centrato sul colorethumbClass, thumbStyle
ColorSlideril pickerclass, style
La traccia dello sliderlo slidertrackClass, trackStyle
Il thumb dello sliderlo slider, centrato sul valore sulla tracciathumbClass, thumbStyle
ColorFieldil pickerclass, style
ColorSwatchil picker, oppure da soloclass, style, selectedStyle
ColorEyeDropperil picker, oppure da soloclass, style

Il thumb di uno slider è centrato sulle estremità della traccia in corrispondenza delle estremità del canale. Un padding sullo slider pari a metà della larghezza del thumb (h-6 justify-center px-2.5 attorno a una traccia h-3) mantiene l’intero thumb dentro lo slider, dove riceve le pressioni a entrambe le estremità. Gli stati mostrati dalle parti: active: di un thumb mentre viene trascinato e focus: con il focus da tastiera, selected: di un campione, e disabled: di ogni parte.

#Tastiera

Su macOS e sul web ogni thumb è una tappa del focus da tastiera (Tab):

TastoSu uno sliderSull’area
Destra, SuUn passo in suDestra: più saturazione; Su: più luminosità
Sinistra, GiùUn passo in giùSinistra: meno saturazione; Giù: meno luminosità
Maiusc con una frecciaDieci passiDieci passi
Pag Su, Pag GiùDieci passiDieci passi di luminosità
Home, FineLe estremità del canale

In un layout da destra a sinistra uno slider orizzontale e l’area partono da destra, con il valore minimo (la minima saturazione) all’estremità destra: Sinistra è un passo in su e Destra un passo in giù, e una pressione o un trascinamento sceglie il valore sotto il puntatore così come è disegnato. Uno slider verticale mantiene Destra e Su come passo in su; Su e Giù sull’area mantengono la luminosità.

Un campione accetta Spazio e Invio. Ogni tasto segnala onChange e poi onChangeEnd; le frecce si ripetono se tenute premute.

#Accessibilità

  • Il thumb dell’area e quello di ogni slider sono elementi regolabili ("adjustable", uno slider nel browser) con la loro etichetta, valore e intervallo; l’incremento e il decremento degli screen reader li fanno avanzare di un passo.
  • Ogni campo è un campo di testo con la sua etichetta; ogni campione un pulsante, selezionato mentre è il colore del picker, denominato dal suo colore in esadecimale a meno che accessibilityLabel non lo denomini. Il contagocce è un pulsante con l’etichetta dell’app.
  • Quando sono disabilitate, gli screen reader indicano che le parti sono attenuate, ed esse non accettano azioni.

Vedi Accessibilità per etichette, ruoli e azioni in generale.

#Note sulle piattaforme

  • Ogni piattaforma disegna ciò che l’app definisce nello stile; i colori delle tracce e del pannello sono gradienti disegnati dalla piattaforma. Pressioni e trascinamenti sono seguiti nel core, per un mouse, una penna o un dito, e una scroll view attorno al picker limita il proprio scorrimento ai trascinamenti che iniziano altrove.
  • ColorEyeDropper preleva con il color sampler di AppKit su macOS e con EyeDropper del browser sul web; il colore arriva in sRGB.