このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

要素

ColorPicker

カラーピッカーは1つの色を保持し、その内部のパーツが色を表示・変更します。パーツは、彩度と明度のColorArea、RGB、HSL、HSB、OKLCHの任意のチャンネルとアルファのColorSlider、色をテキストとして、または1つのチャンネルを数値として受け取る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、Rustではnsh-baseクレートから提供されます。Rustでは、color_picker()とそのパーツの関数が、プロパティごとのセッター(.channel(..)、.thumb_style(..))、イベントごとのリスナーメソッド(.on_change(..))、型付きのスタイルメソッドでこれらを生成します。色はHslaで、色空間、チャンネル、フォーマットはそれぞれColorSpace、ColorChannel、ColorFormatです。

#値

色はCSSカラーです。16進数(#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 })は、変更が終わった時点の色を1回だけ報告します。ポインターを離したとき、各キー入力の後、フィールドからフォーカスが外れたか送信されたとき、スウォッチが押されたときです。
  • ピッカーは、各色空間の色をユーザーが設定したとおりに保持します。グレーは色相と彩度を保持するため、色相スライダーはユーザーが残した位置にとどまります。OKLCHで編集した色は、sRGBで表示できる範囲を超えても、入力された明度、クロマ、色相を保持します(報告される色はsRGBで表示できる最も近い色で、"oklch"のformattedはチャンネルの値を保持します)。
  • アプリが設定した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刻み)、クロマは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(..)string16進数の色スクリーンリーダーが読み上げる内容。
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>なしボタン。

押すとシステムのスポイトが表示されます。macOSではAppKitのカラーサンプラー、Webではブラウザの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 })変更が終了したとき。変更ごとに1回。
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-3のトラックの周囲にh-6 justify-center px-2.5)を設けると、サム全体がスライダー内に収まり、両端でも押下を受け付けます。パーツが示す状態は、ドラッグ中のサムのactive:、キーボードフォーカス時のfocus:、スウォッチのselected:、そしてすべてのパーツのdisabled:です。

#キーボード

macOSとWebでは、各サムがキーボードフォーカス(Tab)の停止位置になります。

キースライダー上エリア上
Right、Up1ステップ増加Right: 彩度を上げる、Up: 明度を上げる
Left、Down1ステップ減少Left: 彩度を下げる、Down: 明度を下げる
Shiftと矢印キー10ステップ10ステップ
Page Up、Page Down10ステップ明度を10ステップ
Home、Endチャンネルの両端

右から左へのレイアウトでは、水平スライダーとエリアは右から始まり、最小値(最小の彩度)が右端になります。Leftが1ステップ増加、Rightが1ステップ減少になり、押下やドラッグでは描画されたとおりにポインター下の値が選ばれます。垂直スライダーでは、RightとUpが引き続き1ステップ増加です。エリアのUpとDownは明度を操作します。

スウォッチはSpaceとReturnを受け付けます。各キーはonChangeに続いてonChangeEndを報告します。矢印キーは押し続けると繰り返されます。

#アクセシビリティ

  • エリアと各スライダーのサムは調整可能な要素("adjustable"、ブラウザではスライダー)で、ラベル、値、範囲を持ちます。スクリーンリーダーの増加・減少操作でステップ単位に変更できます。
  • 各フィールドはラベル付きのテキストフィールドです。各スウォッチはボタンで、ピッカーの色である間は選択状態になり、accessibilityLabelで名前を指定しない限り16進数の色が名前になります。スポイトは、アプリのラベルを持つボタンです。
  • 無効の間、スクリーンリーダーはパーツが淡色表示であると伝え、操作は受け付けません。

ラベル、ロール、アクション全般については、アクセシビリティを参照してください。

#プラットフォームに関する注記

  • どのプラットフォームでも、アプリがスタイルしたものが描画されます。トラックとパネルの色は、プラットフォームが描画するグラデーションです。押下とドラッグは、マウス、ペン、指のいずれでもコアで追跡され、ピッカーを囲むスクロールビューのスクロールは、ほかの場所から始まったドラッグに限られます。
  • ColorEyeDropperは、macOSではAppKitのカラーサンプラー、WebではブラウザのEyeDropperで色を選びます。色はsRGBで渡されます。