# ColorPicker

Chọn màu bất kỳ. Trigger hiện màu hiện tại; nó mở một panel với vùng độ bão hòa và độ sáng, một thanh sắc độ và một ô hex. value luôn là chuỗi hex 6 chữ số.

**Phụ thuộc:** npm: 0 trực tiếp, 7 tổng; registry: 6 trực tiếp, 9 tổng.

## Cách dùng

### Khi nào dùng

- Màu bất kỳ đều là đáp án hợp lệ: màu thương hiệu hoặc accent, màu nhãn, màu series của biểu đồ.
- Màu mà người dùng muốn thấy trang đổi trong lúc kéo.

### Khi nào không dùng

- Vài màu có tên. Dùng RadioGroup với swatch.
- Màu có độ trong suốt. Nó chỉ chọn màu đục.
- Giá trị hex gõ từ một file thiết kế, không có gì khác để chỉnh. Dùng Input.

## Biến thể

### variant

input hiện swatch và hex như một field, cho form. swatch là button vuông chỉ có swatch, cho thanh công cụ và header. inline là chính panel chọn (vùng, thanh sắc độ, ô hex, preset) được render tại chỗ, không trigger và không lớp riêng, để nhúng vào một bề mặt khác chẳng hạn Sheet.

Giá trị: `input`, `swatch`, `inline`. Mặc định: `input`.

### size

Chiều cao trigger theo size-element-*, giống Input. Panel không đổi theo size.

Giá trị: `sm`, `md`, `lg`, `xl`. Mặc định: `md`.

### align

Căn panel theo trigger, giống Popover. Bị bỏ qua dưới md, lúc panel là Sheet phía dưới.

Giá trị: `start`, `center`, `end`. Mặc định: `start`.

## Ví dụ

### Cơ bản

```tsx
<ColorPicker aria-label="Màu thương hiệu" defaultValue="#7c5cff" />
```

### Trigger có tên

```tsx
<ColorPicker aria-label="Màu nhấn" triggerLabel="Màu nhấn" />
```

### Xem trước trực tiếp, lưu khi thả

```tsx
<ColorPicker aria-label="Màu nhấn" defaultValue="#7c5cff" onValueChange={previewColor} onValueCommit={saveColor} />
```

### Trigger swatch với preset

```tsx
<ColorPicker aria-label="Màu nhãn" variant="swatch" size="sm" swatches={["#d93a3a", "#e0a020", "#2a9d5c", "#2f7de1", "#7c5cff"]} defaultValue="#2a9d5c" />
```

### Vô hiệu

```tsx
<ColorPicker aria-label="Màu thương hiệu" defaultValue="#7c5cff" disabled />
```

## Nên và không nên

### Nên

- Dùng nó khi màu bất kỳ đều là đáp án hợp lệ: màu thương hiệu hoặc accent, màu nhãn, màu series của biểu đồ.
- Lưu ở onValueCommit. Dùng onValueChange chỉ để xem trước trực tiếp.
- Gắn aria-label nói màu này dùng để làm gì.
- Dùng variant swatch ở chỗ chật như header hoặc thanh công cụ, variant input trong form.
- Để việc sửa contrast, lịch sử như màu vừa dùng, và việc lưu vào storage trong ứng dụng. Component chỉ chọn một hex.
- Truyền aria-label, areaLabel, hueLabel, saturationLabel, brightnessLabel và hexLabel đã dịch trên trang không phải tiếng Anh.

### Không nên

- Đừng dùng nó để chọn giữa vài màu có tên. Dùng RadioGroup hoặc SegmentedControl với swatch màu.
- Đừng thêm thư viện màu cho nó. Đổi giữa hex, RGB và HSV chỉ là vài dòng bên trong component.
- Đừng dùng input màu native bên trong nó; nó trông khác trên mỗi nền tảng.
- Đừng đổi theme ở mỗi onValueChange mà không giới hạn còn một lần cập nhật mỗi frame.
- Đừng đặt nó trong phần chân của Dialog hoặc trong Menu; panel cần chỗ riêng.

## Props

| Tên | Kiểu | Giá trị | Mặc định | Mô tả |
| --- | --- | --- | --- | --- |
| `value` | `string` |  |  | Màu được kiểm soát, hex 6 chữ số viết thường như "#7c5cff". Ghép với onValueChange. |
| `defaultValue` | `string` |  | `#111111` | Màu ban đầu khi không kiểm soát. |
| `onValueChange` | `function` |  |  | (value: string) => void. Chạy mỗi lần đổi khi đang kéo, mỗi bước phím và khi commit hex hợp lệ. Có thể chạy nhiều lần mỗi giây; bên gọi quyết định có giới hạn tần suất hay không. |
| `onValueCommit` | `function` |  |  | (value: string) => void. Chạy một lần khi thả con trỏ, sau một bước phím, khi commit hex hợp lệ, và khi panel đóng với màu đã đổi. Lưu hoặc gửi value ở đây, không phải trong onValueChange. |
| `open` | `boolean` |  |  | Trạng thái mở được kiểm soát của panel. Ghép với onOpenChange. |
| `defaultOpen` | `boolean` |  | `false` | Trạng thái mở ban đầu khi không kiểm soát. |
| `onOpenChange` | `function` |  |  | (open: boolean) => void. |
| `variant` | `enum` | `input`, `swatch`, `inline` | `input` | input hiện swatch và hex như một field, cho form. swatch là button vuông chỉ có swatch, cho thanh công cụ và header. inline là chính panel chọn (vùng, thanh sắc độ, ô hex, preset) được render tại chỗ, không trigger và không lớp riêng, để nhúng vào một bề mặt khác chẳng hạn Sheet. |
| `triggerLabel` | `string` |  |  | Chữ nhìn thấy thay cho hex trên trigger input, chẳng hạn Accent. Swatch vẫn hiện và tên truy cập vẫn gồm hex hiện tại. Bị bỏ qua trên trigger swatch. |
| `size` | `enum` | `sm`, `md`, `lg`, `xl` | `md` | Chiều cao trigger theo size-element-*, giống Input. Panel không đổi theo size. |
| `swatches` | `string` |  |  | string[] các giá trị hex tùy chọn, hiện như preset dưới ô hex, ví dụ ["#d93a3a", "#2a9d5c"]. Để trống để ẩn hàng. (Schema của spec không có kiểu mảng, nên khai báo là string.) |
| `showHexField` | `boolean` |  | `true` | Hiện ô hex. Đây là cách chính để người dùng trình đọc màn hình chọn màu, nên chỉ tắt khi một chỗ nhập chữ khác cho cùng value nằm ngay cạnh bộ chọn. |
| `align` | `enum` | `start`, `center`, `end` | `start` | Căn panel theo trigger, giống Popover. Bị bỏ qua dưới md, lúc panel là Sheet phía dưới. |
| `areaLabel` | `string` |  | `Saturation and brightness` | Tên truy cập của thumb vùng. Truyền chuỗi đã dịch ở ngôn ngữ khác. |
| `hueLabel` | `string` |  | `Hue` | Tên truy cập của thumb sắc độ. |
| `saturationLabel` | `string` |  | `saturation` | Từ của trục được đọc trong aria-valuetext của vùng, sau hex, ví dụ "#7c5cff, saturation 64%". |
| `brightnessLabel` | `string` |  | `brightness` | Từ của trục được đọc trong aria-valuetext của vùng khi độ sáng đổi, ví dụ "#7c5cff, brightness 40%". |
| `hexLabel` | `string` |  | `Hex` | Nhãn nhìn thấy của ô hex. |
| `hexPlaceholder` | `string` |  |  | placeholder của ô hex. |
| `hexError` | `string` |  |  | Hiện dưới ô hex, với role="alert", khi chữ đã gõ không phải một màu. Truyền bản đã dịch. |
| `title` | `string` |  |  | Tiêu đề nhìn thấy ở đầu panel. Nó cũng đặt tên cho Sheet dưới md; không có nó thì Sheet được đặt tên bằng aria-label, ẩn thị giác. |
| `children` | `node` |  |  | Nội dung riêng của ứng dụng dưới các preset, chẳng hạn liên kết đặt lại, một dòng trợ giúp hoặc các button xuất. Bản thân component không giữ chữ nào ngoài các prop label của nó. |
| `disabled` | `boolean` |  | `false` | Trigger không focus hoặc mở được. |
| `invalid` | `boolean` |  | `false` | Đánh dấu trigger là invalid trong form (aria-invalid). Ô hex cũng tự hiện invalid khi chữ đã gõ không phải một màu. |
| `name` | `string` |  |  | Tên field trong form. Render một input ẩn với hex đã commit. |

## Cấu tạo

| Phần | Mô tả |
| --- | --- |
| `root` | ColorPicker. Giữ value và trạng thái mở; bản thân không render gì. Dựng trên Popover. |
| `trigger` | ColorPickerTrigger: button mở panel. variant input trông như Input (cùng nền nổi, viền, quầng focus và kích thước) với swatch ở đầu và hex bằng font-family-mono ngay sau. variant swatch là button vuông cỡ size-element-* chỉ có swatch bên trong, cho thanh công cụ và header. triggerLabel thay hex nhìn thấy bằng một nhãn giao diện bằng font-family-sans, vẫn giữ hex trong tên truy cập. |
| `swatch` | Ô vuông của màu hiện tại, 16px ở sm, md và lg, 20px ở xl, radius-inner, mép 1px color-border-strong để trắng và màu gần nền trang vẫn thấy viền. Phần tô là chính value, không phải token. Khi disabled, nó mờ xuống opacity-disabled, vì không token nào làm xám được value. |
| `content` | ColorPickerContent: từ md trở lên, PopoverContent với cùng bề mặt, viền, bo góc, đổ bóng và padding như Popover, rộng 288px (không bao giờ rộng hơn màn hình trừ spacing-sm mỗi bên). Dưới md, một Sheet với side="bottom" chứa cùng children, hết chiều ngang trừ padding của Sheet. Children xếp với gap spacing-md. Bộ chọn disabled được mở từ code thì làm mờ cả panel xuống opacity-disabled (chính panel mang data-disabled, vì nó nằm trong portal). variant inline render cùng nội dung tại chỗ: không khung panel, không trigger và không lớp riêng, nên Sheet hoặc popover chủ nhúng được vùng chọn trực tiếp. |
| `area` | ColorPickerArea: vùng độ bão hòa (x) và độ sáng (y) cho sắc độ hiện tại. Hết chiều ngang nội dung, tỉ lệ 8:5 trong cả Popover và Sheet (256 trên 160 ở 288px), radius-inner, touch-action none để kéo không bao giờ cuộn trang hoặc vuốt đóng Sheet. Gradient của nó được tính từ sắc độ, không phải token. |
| `areaThumb` | Vòng tròn 16px đánh dấu độ bão hòa và độ sáng hiện tại: tô đặc color-background-raised, mép 1px color-border-strong, shadow-low. Phần tô raised và viền strong là một cặp sáng và tối ở cả hai chế độ, nên thumb hiện trên mọi màu. Điểm chọn là tâm; nó có thể tràn mép vùng bằng nửa chiều rộng. |
| `hue` | ColorPickerHue: thanh ngang của mọi sắc độ, hết chiều ngang nội dung, cao 12px, radius-full, touch-action none. Gradient của nó được tính, không phải token. |
| `hueThumb` | Cùng thumb 16px như areaThumb, căn giữa theo chiều dọc trên thanh. |
| `hexField` | ColorPickerInput: Input, font-family-mono, swatch là slot phía trước, maxLength 7. Size md trong Popover, lg trong Sheet. Commit khi Enter và khi blur; Escape trả về giá trị hợp lệ cuối. |
| `swatches` | ColorPickerSwatches: hàng màu preset tùy chọn dưới ô hex, gap spacing-xs, tự xuống dòng. Mỗi preset là button radius-inner với cùng viền như swatch: 20px trong Popover, size-element-lg trong Sheet. Preset nào bằng value thì được chọn. |
| `hitArea` | ::before trong suốt, căn giữa trên trigger, mỗi thumb và mỗi preset, không bao giờ nhỏ hơn size-target-min. |

## Khả năng truy cập

Role: `button`

| Phím | Hành động |
| --- | --- |
| `Enter / Space` | Trên trigger: mở panel. Focus chuyển tới thumb của vùng. |
| `Escape` | Đóng panel (Popover hoặc Sheet). Focus trở về trigger. Commit màu nếu nó đã đổi. |
| `Tab / Shift+Tab` | Di chuyển giữa thumb vùng, thumb sắc độ, ô hex và các swatch trong panel. |
| `ArrowLeft / ArrowRight` | Trên vùng: độ bão hòa giảm hoặc tăng 1%. Trên thanh sắc độ: sắc độ giảm hoặc tăng 1 độ. |
| `ArrowDown / ArrowUp` | Trên vùng: độ sáng giảm hoặc tăng 1%. Trên thanh sắc độ: giống Left và Right. |
| `Shift+Arrow` | Bước 10% trên vùng, 10 độ trên thanh sắc độ. |
| `PageUp / PageDown` | Giống Shift+Arrow. |
| `Home / End` | Trên vùng: độ bão hòa 0% hoặc 100%. Trên thanh sắc độ: 0 hoặc 359 độ. |
| `Enter` | Trong ô hex: commit giá trị đã gõ nếu hợp lệ. Nếu không hợp lệ, giữ field invalid và màu không đổi. |

- aria-label (hoặc aria-labelledby) là bắt buộc: nó nói màu dùng để làm gì, ví dụ "Accent color". Tên truy cập của trigger là label đó cộng hex hiện tại, ví dụ "Accent color, #7c5cff".
- Thumb vùng và thumb sắc độ đều là role="slider", đặt tên bằng areaLabel và hueLabel. Một slider chỉ có một aria-valuenow: trên vùng nó theo trục đổi lần cuối (độ bão hòa cho Left/Right, độ sáng cho Up/Down), nên mỗi lần nhấn phím đều được đọc lại. aria-valuetext là hex cộng trục đó, dựng từ các prop label, ví dụ "#7c5cff, saturation 64%" hoặc, trên trang tiếng Việt, "#7c5cff, độ bão hòa 64%". Trên thanh sắc độ thì là hex cộng số độ, ví dụ "#7c5cff, 252°".
- Trình đọc màn hình trên điện thoại chỉ có tăng và giảm, nên chỉ tới được một trục của vùng. Ô hex là cách chính để người dùng trình đọc màn hình chọn màu; vùng và thanh sắc độ dành cho pointer và bàn phím. Không bao giờ ẩn ô hex khi không có chỗ nhập chữ khác ở gần.
- Ô hex là một Input được gắn nhãn bằng hexLabel. Nó nhận 3 hoặc 6 chữ số, có hoặc không có #, mọi kiểu hoa thường, và ghi lại hex 6 chữ số viết thường khi commit.
- Mọi thumb và swatch giữ vùng nhấn ::before ít nhất bằng size-target-min (24px với chuột, 44px với cảm ứng).
- Không có kênh alpha trong v1. Không đọc tên màu: component không kèm thư viện màu, nên hex là giá trị được đọc duy nhất.
- Sắc độ giữ trạng thái riêng khi độ bão hòa hoặc độ sáng là 0, nên kéo lên lại từ đen hoặc xám không làm sắc độ nhảy về đỏ.
- Dưới md, panel mở thành Sheet từ dưới lên. Tiêu đề Sheet là aria-label của bộ chọn (ẩn thị giác vẫn được). Đóng bằng vuốt, scrim hoặc Escape thì commit như Escape và trả focus về trigger.
- Mọi chuỗi có sẵn (areaLabel, hueLabel, saturationLabel, brightnessLabel, hexLabel) là một prop để ứng dụng dịch. Component không giữ chữ riêng nào ngoài các mặc định tiếng Anh đó.

## Cài đặt

```bash
npx werewolf-ui add color-picker
```

Chạy `npx werewolf-ui init` một lần trong app trước khi thêm component. Init cài token, thêm CSS import và luật cho agent. Add cài component này cùng các dependency và giữ nguyên theme của bạn. Xem [Cài đặt](https://www.werewolf-ui.dev/vi/docs/installation.md) để biết yêu cầu và cách cài qua registry.
