تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

العناصر

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")، جاهزًا للحفظ أو للضبط كـ token.
  • يبلّغ onChangeEnd({ value, formatted }) عن اللون الذي انتهى عنده التغيير، مرة واحدة: عند رفع المؤشر، وبعد كل مفتاح، وعند مغادرة حقل أو إرساله، وعند الضغط على عيّنة.
  • يحتفظ المنتقي باللون في كل فضاء كما ضبطه المستخدم. يحتفظ الرمادي بصبغته وتشبعه، فيبقى شريط الصبغة حيث تركه المستخدم؛ واللون المحرَّر في OKLCH يحتفظ بإضاءته وكرومته وصبغته كما كُتبت، حتى فيما يتجاوز ما يعرضه 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)، والكروما من 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>لا شيءالزر.

يعرض الضغط قطّارة الألوان الخاصة بالنظام: أداة أخذ العينات اللونية في 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"، وهو slider في المتصفح) مع تسميتهما وقيمتهما ومداهما؛ وتؤدي أوامر الزيادة والنقصان في قارئات الشاشة إلى تحريكهما خطوة خطوة.
  • كل حقل هو حقل نصي مع تسميته؛ وكل عيّنة زر، محددة ما دامت لون المنتقي، وتُسمّى بلونها بصيغة hex ما لم يسمّها accessibilityLabel. أما قطّارة الألوان فهي زر بتسمية من التطبيق.
  • أثناء التعطيل، تذكر قارئات الشاشة أن الأجزاء معتّمة، ولا تقبل أي إجراءات.

راجع إمكانية الوصول للاطلاع على التسميات والأدوار والإجراءات بوجه عام.

#ملاحظات المنصات

  • ترسم كل منصة ما ينسّقه التطبيق؛ وألوان المسارات واللوحة هي تدرجات ترسمها المنصة. تُتابَع الضغطات والسحب في النواة، للفأرة أو القلم أو الإصبع، ويقتصر التمرير في عرض التمرير المحيط بالمنتقي على السحب الذي يبدأ في مكان آخر.
  • يلتقط ColorEyeDropper اللون بأداة أخذ العينات اللونية في AppKit على macOS وبـEyeDropper في المتصفح على الويب؛ ويصل اللون بصيغة sRGB.