ColorPicker#
다양한 형식으로 색상을 선택하고 편집할 수 있는 색상 선택기 컴포넌트입니다. Flutter 와 Web 모두 ColorPicker 로 통일된 API (color
/ onChanged / showAlpha / showLabel / showHexInput
/ pickerMode 등) 를 제공하며, 공통 CoreColorPickerTheme / CoreColorPickerStyle
토큰을 공유합니다. trigger 는 양쪽 모두 [swatch][hex input?] 행으로 동일한 사이즈(40px swatch · 96px hex input · 4px swatch gap) 를 사용합니다.
값 표현은 플랫폼 컨벤션을 따릅니다 — Flutter 는 Color (Color(0xFF3B82F6)), Web 은 #RRGGBB
형식의 hex 문자열. 내부 색상 연산은 양쪽 모두 hex 기반 (core_color_math.dart) 으로 통일되어 있어 정확도와 동작이 동일합니다.
Live Preview#
class ColorPickerDefaultExample extends StatefulComponent {
const ColorPickerDefaultExample({super.key});
@override
State<ColorPickerDefaultExample> createState() =>
_ColorPickerDefaultExampleState();
}
class _ColorPickerDefaultExampleState
extends State<ColorPickerDefaultExample> {
String _color = '#3B82F6';
@override
Component build(BuildContext context) {
return ColorPicker(
color: _color,
showHexInput: true,
onChanged: (value) {
setState(() => _color = value);
},
);
}
}
class ColorPickerDefaultExample extends StatefulWidget {
const ColorPickerDefaultExample({super.key});
@override
State<ColorPickerDefaultExample> createState() =>
_ColorPickerDefaultExampleState();
}
class _ColorPickerDefaultExampleState
extends State<ColorPickerDefaultExample> {
Color _color = const Color(0xFF3B82F6);
@override
Widget build(BuildContext context) {
return ColorPicker(
color: _color,
showHexInput: true,
onChanged: (value) {
setState(() => _color = value);
},
);
}
}
class ColorPickerChainExample extends StatefulComponent {
const ColorPickerChainExample({super.key});
@override
State<ColorPickerChainExample> createState() => _ColorPickerChainExampleState();
}
class _ColorPickerChainExampleState extends State<ColorPickerChainExample> {
String _color = '#3B82F6';
@override
Component build(BuildContext context) {
return ColorPicker(
color: _color,
showHexInput: true,
onChanged: (value) {
setState(() => _color = value);
},
).withStyle(
const CoreColorPickerStyle(
swatchSize: CoreSpace.space48,
hexInputWidth: CoreSpace.space112,
swatchGapStyle: CoreGapStyle(size: CoreSpace.space12),
swatchBorderRadius: CoreBorderRadius.all(CoreRadius.radius16),
borderColor: CoreColor.token(CoreColors.primary),
borderWidth: CoreStrokeWidth.stroke2,
),
);
}
}
class ColorPickerChainExample extends StatefulWidget {
const ColorPickerChainExample({super.key});
@override
State<ColorPickerChainExample> createState() => _ColorPickerChainExampleState();
}
class _ColorPickerChainExampleState extends State<ColorPickerChainExample> {
Color _color = const Color(0xFF3B82F6);
@override
Widget build(BuildContext context) {
return ColorPicker(
color: _color,
showHexInput: true,
onChanged: (value) {
setState(() => _color = value);
},
).withStyle(
const CoreColorPickerStyle(
swatchSize: CoreSpace.space48,
hexInputWidth: CoreSpace.space112,
swatchGapStyle: CoreGapStyle(size: CoreSpace.space12),
swatchBorderRadius: CoreBorderRadius.all(CoreRadius.radius16),
borderColor: CoreColor.token(CoreColors.primary),
borderWidth: CoreStrokeWidth.stroke2,
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 사용자가 브랜드 색상, 테마 색상 등 임의의 색상을 직접 지정해야 하는 경우
- 디자인 도구, 에디터 등에서 색상 팔레트를 제공해야 하는 경우
- HEX 값이나 RGB / HSL / HSV 중 특정 색공간으로 값을 입력받아야 하는 경우
대신 다른 컴포넌트를 사용하세요:
Select: 고정된 색상 목록에서만 선택해야 하는 경우TextField: 색상 코드를 순수 텍스트로만 입력받는 경우
기본 사용법 (Basic Usage)#
// Color 값 기반 — Flutter 컨벤션
ColorPicker(
color: Color(0xFF3B82F6),
onChanged: (color) => setState(() => _color = color),
showHexInput: true,
)
// RGB / HSL / HSV 기본 모드 + 알파 슬라이더 + 화면 색 추출
ColorPicker(
color: _color,
onChanged: handleColorChanged,
showAlpha: true,
showLabel: true,
pickerMode: CoreColorPickerMode.rgb,
enableEyeDropper: true,
)
// hex 문자열 기반 — CSS 컨벤션
ColorPicker(
color: '#3B82F6',
onChanged: (hex) => setState(() => _color = hex),
showHexInput: true,
)
ColorPicker(
color: _color,
onChanged: handleColorChanged,
showAlpha: true,
showLabel: true,
pickerMode: CoreColorPickerMode.rgb,
enableEyeDropper: true,
)
Props / Parameters#
공통 Contract 필드 (CoreColorPickerContract):
| 속성 | Flutter | Web | 기본값 | 설명 |
|---|---|---|---|---|
color |
Color |
String |
필수 | 현재 선택된 색상 (Flutter Color, Web #RRGGBB) |
onChanged |
ValueChanged<Color>? |
CoreValueChanged<String>? |
null |
Save 커밋 시 호출되는 콜백 (live-preview 에는 호출되지 않음) |
enabled |
bool? |
bool? |
true |
인터랙션 가능 여부 |
showAlpha |
bool? |
bool? |
false |
알파 슬라이더 표시 |
showLabel |
bool? |
bool? |
false |
swatch 옆 hex 라벨 표시 |
showHexInput |
bool? |
bool? |
false |
swatch 옆 hex 텍스트 입력 표시 |
pickerMode |
CoreColorPickerMode? |
CoreColorPickerMode? |
rgb |
RGB / HSL / HSV 색공간 |
enableEyeDropper |
bool? |
bool? |
true |
EyeDropper 버튼 표시 (Web 은 Chromium 95+ / Flutter 는 web target 에서 동작) |
promptMode |
CoreColorPickerPromptMode? |
CoreColorPickerPromptMode? |
popover |
트리거 클릭 시 popover/dialog 중 어떤 surface로 열릴지 |
dialogTitle |
Widget? |
Component? |
null |
팝오버 / 다이얼로그 상단 타이틀 |
placeholder |
Widget? |
Component? |
null |
미선택 상태 placeholder |
controller |
Object? |
Object? |
null |
외부 imperative controller |
colorPickerStyle |
CoreColorPickerStyle? |
CoreColorPickerStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
ColorPicker 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreColorPickerStyle 단일 슬롯으로 흐릅니다. 평면 chrome 필드 (popoverAlignment
/ popoverAnchorAlignment / popoverPadding) 는 모두 제거되었습니다.
시맨틱 vs 스타일#
-
시맨틱 enum / behaviour: 위젯/컴포넌트 파라미터로 직접 (
color,onChanged,enabled,showAlpha,showLabel,showHexInput,pickerMode,enableEyeDropper,promptMode,dialogTitle,placeholder,controller) -
chrome / dimensional / 슬롯 스타일:
CoreColorPickerStyle한 곳으로 (popoverStyle/dialogStyle/swatchButtonStyle/hexInputStyle/labelStyle/descriptionStyle/errorStyle) -
변형(variant) 교체: asChild —
dialogTitle/placeholder슬롯 등에 직접 위젯 주입
Resolve chain#
design system default for color picker
→ CoreColorPickerTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.colorPickerStyle // 인스턴스별
각 nested 슬롯 스타일 (popoverStyle / dialogStyle / swatchButtonStyle
/ hexInputStyle) 은 자기 컴포넌트의 자체 resolve chain 으로 다시 한 번 머지됩니다. 예: popoverStyle.triggerStyle
의 chrome 은 → CoreButtonTheme → variant 룩업 → 부모 슬롯 → widget.colorPickerStyle.popoverStyle.triggerStyle
순.
CoreColorPickerStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
swatchSize |
double? |
Trigger swatch button width/height override (logical px). |
hexInputWidth |
double? |
Hex input field width override (logical px). |
swatchGapStyle |
CoreGapStyle? |
Horizontal gap between swatch and hex input override — nested [CoreGapStyle] slot forwarded to
Gap(gapStyle: …)
.
|
popoverWidth |
double? |
Popover panel width override (logical px). |
popoverSpacing |
double? |
Vertical spacing between popover sections override (logical px, pre-scaling). Raw primitive — flows directly into
Column.spacing
(Flutter) and the
flex flex-col
parent's CSS
gap
(Web), not a
Gap
slot. Falls back to [defaultPopoverSpacing] when null.
|
sliderHeight |
double? |
Hue / alpha slider track height override (logical px). |
modeInputSpacing |
double? |
Horizontal spacing between RGB / HSL / HSV mode input cells override (logical px, pre-scaling). Raw primitive — flows directly into
Row.spacing
(Flutter) and the
flex
parent's CSS
gap
(Web), not a
Gap
slot. Falls back to [defaultModeInputSpacing] when null.
|
modeInputLabelGapStyle |
CoreGapStyle? |
Vertical 1-off gap between a mode input cell's label and its number field override — nested [CoreGapStyle] slot forwarded to
Gap(gapStyle: …)
. The label/field stacking is a single 1-off spacer internal to the mode input cell on both platforms.
|
actionRowGapStyle |
CoreGapStyle? |
Horizontal gap between Cancel / Save buttons in the action row override — nested [CoreGapStyle] slot forwarded to
Gap(gapStyle: …)
.
|
eyeDropperIconStyle |
CoreIconStyle? |
Eye-dropper button icon style override — nested [CoreIconStyle] slot forwarded to
Icon(iconStyle: …)
. Falls back to [defaultEyeDropperIconStyle] when null.
|
recentSwatchSize |
double? |
Recent-color swatch side length override (logical px). |
recentSwatchSpacing |
double? |
Spacing (logical px) between recent-color swatches override — raw primitive, drives
Wrap.spacing/runSpacing
(Flutter) and CSS
gap
(Web), not a
Gap
slot.
|
recentColumns |
int? |
Recent-colors grid column count override. |
swatchBorderRadius |
CoreBorderRadius? |
Corner radius override for the trigger swatch button, hue / alpha sliders, and recent-color swatches. |
saturationValueBorderRadius |
CoreBorderRadius? |
Corner radius override for the 2D saturation/value area inside the popover. |
borderColor |
CoreColor? |
Border colour override for the trigger swatch, the 2D saturation/value area, and the hue / alpha slider tracks. Falls back to [defaultBorderColor] (
outline
token) when null.
|
borderWidth |
double? |
Border stroke width override (logical px) for the trigger swatch, the 2D saturation/value area, and the hue / alpha slider tracks. Falls back to [defaultBorderWidth] when null. |
disabledBorderColor |
CoreColor? |
Border colour override for the trigger swatch when disabled. Falls back to [defaultDisabledBorderColor] (
disabledOutline
token) when null.
|
selectedRingColor |
CoreColor? |
Ring colour override for the currently-selected recent-color swatch. Falls back to [defaultSelectedRingColor] (
primary
token) when null.
|
recentSwatchBorderWidth |
double? |
Border stroke width override (logical px) for an unselected recent-color swatch. Falls back to [defaultRecentSwatchBorderWidth] when null. |
recentSwatchSelectedBorderWidth |
double? |
Border stroke width override (logical px) for the currently-selected recent-color swatch. Falls back to [defaultRecentSwatchSelectedBorderWidth] when null. |
transitionDuration |
Duration? |
Transition duration override for the swatch hover / focus colour transitions and the recent-swatch ring transition. Falls back to [defaultTransitionDuration] when null. |
popoverStyle |
CorePopoverStyle? |
Popover style for the popover-mode picker (trigger + dropdown chrome). |
dialogStyle |
CoreDialogStyle? |
Dialog style for the modal-mode picker.
Read by no resolver and no widget — wire it or remove it.
Wire it, don't default it. Both resolvers forward it (
dialogStyle: merged.dialogStyle
) into
ResolvedColorPicker.dialogStyle
, but no widget reads that field and neither platform composes a
Dialog
:
promptMode:.dialog
renders the same popover content column (Flutter
_buildPopoverContent
-equivalent SizedBox+Column, Web
_buildPopoverContent
), only with
dialogTitle
prepended. So the real defect is the missing
Dialog
composition for
promptMode:.dialog
(style-contract.md facet 4: modal centre surface must be
Dialog
, not a re-drawn panel) — fixing that makes the slot live and gives it a natural partial default. Until then it must NOT get a default: a value on a slot nothing reads is an orphan constant, the mirror violation. Deletion would also be viable but is a breaking public-field removal and not this PR's call. NOTE:
CoreColorPickerStyle.defaultHexInputStyle
's doc comment already asserts 'no platform composes a Dialog' — that claim is correct and is corroborated here.
|
swatchButtonStyle |
CoreButtonStyle? |
Style applied to each preset / history color swatch button. Carries no
defaultSwatchButtonStyle
. No platform composes a
Button
for the swatch — both read exactly two sub-fields off this slot as the
first rung of a precedence chain
over the flat swatch geometry:
merged.swatchButtonStyle?.width ?? merged.swatchSize ?? defaultSwatchSize
and
merged.swatchButtonStyle?.borderRadius ?? merged.swatchBorderRadius ?? defaultSwatchBorderRadius
. A default setting either sub-field would outrank [defaultSwatchSize] / [defaultSwatchBorderRadius] and make those two named defaults — the values actually painted — unreachable.
|
recentSwatchClickableStyle |
CoreClickableStyle? |
Nested style for the composed recent-color swatch
Clickable
— carries any press / focus-ring / cursor chrome overrides for the swatch affordance. Merged on top of [defaultRecentSwatchClickableStyle] and raw-forwarded to
Clickable(clickableStyle:)
.
|
hexInputStyle |
CoreTextFieldStyle? |
Style applied to the hex input field (showHexInput: true). |
labelStyle |
CoreTextStyle? |
Label text style (text shown above the trigger).
Read by no resolver and no widget — wire it or remove it.
Wire it or delete it — do not default it. Forwarded into
ResolvedColorPicker.labelStyle
on both platforms and read by nobody:
ColorPicker
has no
label
slot at all (its
showLabel
bool only gates the hex
TextField
's visibility alongside
showHexInput
, and the internal mode-input captions use a
Text(...).bodySmall.onSurfaceVariant
chain, not this slot). Cleanest resolution is to let ColorPicker compose
FormField
, which already owns label/description/error and their defaults, and drop these three slots; otherwise add real
label
widget slots on both platforms. A default on an unread slot would just be an orphan constant.
|
descriptionStyle |
CoreTextStyle? |
Description / helper text style.
Read by no resolver and no widget — wire it or remove it.
Same as
labelStyle
: forwarded into
ResolvedColorPicker.descriptionStyle
on both platforms, read by nobody, and
ColorPicker
renders no description/helper text node on either platform. Route through a composed
FormField
(which owns
defaultDescriptionTextStyle
) or remove in a breaking release; do not give it a value while nothing reads it.
|
errorStyle |
CoreTextStyle? |
Error text style.
Read by no resolver and no widget — wire it or remove it.
Same as
labelStyle
: forwarded into
ResolvedColorPicker.errorStyle
on both platforms, read by nobody, and
ColorPicker
renders no error text node on either platform. Per widget/failure-surfaces.md the error surface belongs to the composed
FormField
/
ValidatorHint
, so route it there or remove in a breaking release; do not give it a value while nothing reads it.
|
Migration — 옛 평면 필드 → 새 위치 매핑#
| 옛 chrome 필드 | 새 위치 |
|---|---|
popoverAlignment |
popoverStyle.placement (또는 Popover 의 placement 직접 지정) |
popoverAnchorAlignment | popoverStyle.placement |
popoverPadding | popoverStyle.panelPadding |
사용 예 (Flutter)#
ColorPicker(
color: const Color(0xFF3B82F6),
showHexInput: true,
onChanged: handleColorChanged,
colorPickerStyle: CoreColorPickerStyle(
popoverStyle: CorePopoverStyle(
panelBorderRadius: 12,
panelPadding: 16,
gap: 8,
),
swatchButtonStyle: CoreButtonStyle(
borderRadius: 4,
),
hexInputStyle: CoreTextFieldStyle(
borderColor: Color(0xFF3B82F6),
),
labelStyle: CoreTextStyle(fontWeight: 600),
),
)
사용 예 (Web)#
ColorPicker(
color: '#3B82F6',
showHexInput: true,
onChanged: handleColorChanged,
colorPickerStyle: CoreColorPickerStyle(
popoverStyle: CorePopoverStyle(
panelBorderRadius: 12,
panelPadding: 16,
),
hexInputStyle: CoreTextFieldStyle(
borderColor: 'primary',
),
),
)
빠른 오버라이드 (Chain)#
이미 만든 ColorPicker 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class ColorPickerChainExample extends StatefulWidget {
const ColorPickerChainExample({super.key});
@override
State<ColorPickerChainExample> createState() => _ColorPickerChainExampleState();
}
class _ColorPickerChainExampleState extends State<ColorPickerChainExample> {
Color _color = const Color(0xFF3B82F6);
@override
Widget build(BuildContext context) {
return ColorPicker(
color: _color,
showHexInput: true,
onChanged: (value) {
setState(() => _color = value);
},
).withStyle(
const CoreColorPickerStyle(
swatchSize: CoreSpace.space48,
hexInputWidth: CoreSpace.space112,
swatchGapStyle: CoreGapStyle(size: CoreSpace.space12),
swatchBorderRadius: CoreBorderRadius.all(CoreRadius.radius16),
borderColor: CoreColor.token(CoreColors.primary),
borderWidth: CoreStrokeWidth.stroke2,
),
);
}
}
class ColorPickerChainExample extends StatefulComponent {
const ColorPickerChainExample({super.key});
@override
State<ColorPickerChainExample> createState() => _ColorPickerChainExampleState();
}
class _ColorPickerChainExampleState extends State<ColorPickerChainExample> {
String _color = '#3B82F6';
@override
Component build(BuildContext context) {
return ColorPicker(
color: _color,
showHexInput: true,
onChanged: (value) {
setState(() => _color = value);
},
).withStyle(
const CoreColorPickerStyle(
swatchSize: CoreSpace.space48,
hexInputWidth: CoreSpace.space112,
swatchGapStyle: CoreGapStyle(size: CoreSpace.space12),
swatchBorderRadius: CoreBorderRadius.all(CoreRadius.radius16),
borderColor: CoreColor.token(CoreColors.primary),
borderWidth: CoreStrokeWidth.stroke2,
),
);
}
}
변형 (Variants)#
swatch + hex 텍스트 입력#
ColorPicker(
color: const Color(0xFF3B82F6),
showHexInput: true,
onChanged: handleChanged,
)
EyeDropper#
ColorPicker(
color: _color,
enableEyeDropper: true,
onChanged: handleChanged,
)
enableEyeDropper: true + 호스트가 window.EyeDropper API 를 지원하면 액션 행에 스포이드 버튼 표시. Chromium 95+ / Flutter Web target 에서 동작. 미지원 환경 (Firefox · Safari · 네이티브 Flutter) 에서는 버튼이 자동 숨김.
최근 색 (Recent Colors)#
ColorPicker 는 같은 화면 안에서 commit 한 색상을 RecentColorsScope 를 통해 공유합니다. picker 호스팅 화면을 scope 로 wrap 하면 그 안의 모든 picker 가 동일 history 를 보고, Save 액션이 새 색을 push 하면 다른 picker 의 그리드에 즉시 반영됩니다.
// Flutter
RecentColorsScope(
initialRecentColors: [
CoreColor.fromHex('#3B82F6'),
CoreColor.fromHex('#E11D48'),
],
maxRecentColors: 10,
onRecentColorsChanged: (colors) => persist(colors),
child: ColorPaletteEditor(...),
)
// Web — 동일 시그니처
RecentColorsScope(
initialRecentColors: [
CoreColor.fromHex('#3B82F6'),
CoreColor.fromHex('#E11D48'),
],
maxRecentColors: 10,
onRecentColorsChanged: (colors) => persist(colors),
child: ColorPaletteEditor(...),
)
scope 바깥에서 단독으로 쓰는 picker (예: 스토리북, 단일 다이얼로그) 는 history 그리드 영역이 자동으로 숨겨지고 onChanged Save 콜백만 발사됩니다. App-level prop 이 아닌 picker 호스팅 화면 단위로 wrap 하는 패턴이 정공법 — root 가 아닌 scope 가 필요한 곳에서만 적용됩니다.
그리드 토큰#
그리드 swatch 사이즈와 컬럼 수는 CoreColorPickerStyle 의 defaultRecentSwatchSize / defaultRecentSwatchGap
/ defaultRecentColumns 에서 결정됩니다 (기본 24px swatch · 4px gap · 10 columns). 양 플랫폼 grid 가 같은 토큰 source 를 참조합니다.
동작 스펙 (Behavior)#
인터랙션#
- swatch 클릭: 통일된
Popover팝오버 열림 (양 플랫폼 동일 인프라) - hex 입력:
#RRGGBB형식 검증 후_commit발사 -
mode tab (RGB / HSL / HSV):
Button(.primary | .ghost)사용 — hover / focus / disabled 인터랙션 통일 컴포넌트가 처리 - SV (Saturation / Value) 영역: 마우스 드래그로 saturation / value 동시 조정 (cursor: precise / crosshair)
- hue 슬라이더: 좌우 드래그로 hue 조정 (cursor: ew-resize)
- EyeDropper: 지원 환경에서 화면 픽셀 샘플링
- Outside tap / ESC: Save 안 누른 상태로 닫히면 draft 가 마지막 commit 색으로 자동 롤백
상태 전환#
default→picking: swatch 클릭 시 (Popover open)-
picking→committed: Save 버튼 클릭.onChanged호출 +RecentColorsScope에addHistory -
picking→cancelled: Cancel · outside-tap · ESC. swatch / hex input 이 마지막 committed 색으로 롤백 disabled: 트리거 row 비활성, 팝오버 mount 안 됨 (aria-disabled="true")
색 commit 흐름#
swatch click → popover open → live preview drag/type → Save → _commit(hex)
↓
widget.onChanged(color/hex)
↓
storage.addHistory(CoreColor)
onChanged 는 Save 시점에만 호출되어 의도하지 않은 미커밋 값이 외부 상태로 새지 않습니다.
Theme 오버라이드#
CoreComponentTheme.colorPicker 를 통해 프로젝트 레벨의 기본 스타일 (CoreColorPickerStyle) 을 지정할 수 있습니다. behaviour 기본값 (showAlpha
/ showLabel / showHexInput / pickerMode / enableEyeDropper
/ promptMode) 은 위젯 파라미터로 직접 지정하며, 미지정 시 CoreColorPickerContract.defaultX 단일 출처 기본값을 따릅니다:
CoreComponentTheme(
colorPicker: CoreColorPickerTheme(
style: CoreColorPickerStyle(
popoverStyle: CorePopoverStyle(
panelBorderRadius: 8,
panelPadding: 16,
),
),
),
)
// behaviour 기본값은 위젯 파라미터로:
ColorPicker(
color: color,
onChanged: handleChange,
showAlpha: true,
showHexInput: true,
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
RecentColorsScope 는 picker 를 호스팅하는 화면 단위로 감싸기
RecentColorsScope(
initialRecentColors: [
CoreColor.fromHex('#3B82F6'),
CoreColor.fromHex('#E11D48'),
],
maxRecentColors: 10,
onRecentColorsChanged: (colors) => persist(colors),
child: ColorPaletteEditor(...),
)
App-level(root)에 걸면 서로 무관한 화면의 picker 들이 같은 history 를 공유하게 됩니다 — scope 가 필요한 화면 단위로만 감싸야 의도한 범위에서만 최근 색이 공유됩니다.
❌ Don't#
onChanged를 실시간 드래그 미리보기 트리거로 쓰지 않기
// ❌ SV 영역 드래그·hue 슬라이더 조작마다 호출될 거라 기대
ColorPicker(
color: color,
onChanged: (c) => applyLivePreview(c),
)
onChanged는 Save 커밋 시점에만 호출됩니다 — 팝오버 안에서의 드래그/타이핑 같은 실시간 조작 중에는 호출되지 않으므로, 의도치 않은 미커밋 값이 외부 상태로 새지 않습니다.
접근성 (Accessibility)#
- 트리거 래퍼:
role="group"/aria-label="Color picker" -
스와치:
<button>. 클릭 · 키보드 · 포커스는Popover의 트리거 핸들러가 담당합니다.aria-haspopup="dialog"/aria-expanded는 팝오버 래퍼가 emit합니다. - 팝오버 패널:
role="dialog"/aria-label="Color picker" - 모드 탭:
<button>—Button의 표준 포커스 / 호버 - 비활성:
aria-disabled="true"+disabled
크로스 플랫폼 차이점 (Platform Differences)#
API 와 시각 패리티는 통일됨. 플랫폼 간 차이는 구현 매체 와 값 표현 만 다르며 기능 동등.
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | ColorPicker | ColorPicker |
| 색상 타입 | Color | String (hex) |
| Style 제네릭 | CoreColorPickerStyle |
CoreColorPickerStyle |
| 색공간 선택 | CoreColorPickerMode | CoreColorPickerMode |
| 팝오버 인프라 | Popover (overlay-driven) |
Popover (OverlayHost portal) |
| EyeDropper | Web target 에서 window.EyeDropper API 호출 | 동일 |
| 외부 클릭 / ESC 닫기 | Popover 의 dismissOnOutsideTap / dismissOnEscape |
동일 |
| RecentColorsScope | Data<CoreColorHistoryStorage> InheritedWidget |
_RecentColorsInherited InheritedComponent |