返回 oh-my-ppt
ColorPicker.tsx
根目录 / src / renderer / src / components / ui / ColorPicker.tsx
1 import { useEffect, useRef, useState } from 'react'
2 import { Chrome, ChromeInputType, type ColorResult } from '@uiw/react-color'
3 import { Popover, PopoverContent, PopoverTrigger } from './Popover'
4
5 interface ColorPickerProps {
6 value: string | undefined
7 onChange: (value: string) => void
8 onCommit?: (value: string) => void
9 className?: string
10 allowAlpha?: boolean
11 ariaLabel?: string
12 }
13
14 function parseColor(value: string | undefined): { hex: string; alpha: number } {
15 if (!value) return { hex: '#000000', alpha: 1 }
16
17 const rgbaMatch = value.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\)$/)
18 if (rgbaMatch) {
19 const r = parseInt(rgbaMatch[1])
20 const g = parseInt(rgbaMatch[2])
21 const b = parseInt(rgbaMatch[3])
22 const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1
23 const hex = '#' + [r, g, b].map((color) => color.toString(16).padStart(2, '0')).join('')
24 return { hex, alpha: Math.min(1, Math.max(0, Math.round(a * 100) / 100)) }
25 }
26
27 if (value.startsWith('#')) {
28 if (value.length === 9) {
29 const hex = value.slice(0, 7)
30 const alpha = Math.round((parseInt(value.slice(7, 9), 16) / 255) * 100) / 100
31 return { hex, alpha }
32 }
33 return { hex: value.slice(0, 7), alpha: 1 }
34 }
35
36 return { hex: '#000000', alpha: 1 }
37 }
38
39 function toRgbaString(hex: string, alpha: number): string {
40 const r = parseInt(hex.slice(1, 3), 16)
41 const g = parseInt(hex.slice(3, 5), 16)
42 const b = parseInt(hex.slice(5, 7), 16)
43 return `rgba(${r}, ${g}, ${b}, ${alpha})`
44 }
45
46 function formatColor(hex: string, alpha: number): string {
47 if (alpha >= 1) return hex
48 return toRgbaString(hex, alpha)
49 }
50
51 export function ColorPicker({
52 value,
53 onChange,
54 onCommit,
55 className,
56 allowAlpha = true,
57 ariaLabel
58 }: ColorPickerProps): React.JSX.Element {
59 const { hex, alpha } = parseColor(value)
60 const [open, setOpen] = useState(false)
61 const [draft, setDraft] = useState({ hex, alpha: allowAlpha ? alpha : 1 })
62 const latestColorRef = useRef(formatColor(hex, allowAlpha ? alpha : 1))
63
64 useEffect(() => {
65 const parsed = parseColor(value)
66 const next = { hex: parsed.hex, alpha: allowAlpha ? parsed.alpha : 1 }
67 setDraft(next)
68 latestColorRef.current = formatColor(next.hex, next.alpha)
69 }, [allowAlpha, value])
70
71 const handleChange = (color: ColorResult): void => {
72 const next = {
73 hex: color.hex.toLowerCase(),
74 alpha: allowAlpha ? Math.min(1, Math.max(0, Math.round(color.rgba.a * 100) / 100)) : 1
75 }
76 const nextColor = formatColor(next.hex, next.alpha)
77 setDraft(next)
78 latestColorRef.current = nextColor
79 onChange(nextColor)
80 }
81
82 const displayColor = formatColor(hex, allowAlpha ? alpha : 1)
83
84 return (
85 <div className={className}>
86 <Popover
87 open={open}
88 onOpenChange={(nextOpen) => {
89 setOpen(nextOpen)
90 if (!nextOpen) onCommit?.(latestColorRef.current)
91 }}
92 >
93 <PopoverTrigger asChild>
94 <button
95 type="button"
96 aria-label={ariaLabel}
97 title={value}
98 className="h-8 w-10 shrink-0 cursor-pointer rounded-md border border-[#d7cbb7]/70 bg-[#fffdf8] p-1 transition-colors hover:border-[#879b71] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#5d6b4d]"
99 >
100 <span
101 className="block h-full w-full rounded-[3px] shadow-[inset_0_0_0_1px_rgba(30,38,25,0.12)]"
102 style={{
103 backgroundColor: displayColor,
104 backgroundImage:
105 allowAlpha && alpha < 1
106 ? 'linear-gradient(45deg, #c9c9c9 25%, transparent 25%), linear-gradient(-45deg, #c9c9c9 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #c9c9c9 75%), linear-gradient(-45deg, transparent 75%, #c9c9c9 75%)'
107 : undefined,
108 backgroundSize: allowAlpha && alpha < 1 ? '6px 6px' : undefined,
109 backgroundPosition: allowAlpha && alpha < 1 ? '0 0, 0 3px, 3px -3px, -3px 0' : undefined
110 }}
111 />
112 </button>
113 </PopoverTrigger>
114 <PopoverContent
115 className="color-picker-popover w-[244px] rounded-lg border border-[#d7cbb7]/70 bg-[#fffdf8] p-2 shadow-[0_14px_34px_-12px_rgba(66,53,36,0.3)]"
116 align="start"
117 sideOffset={8}
118 >
119 <Chrome
120 color={allowAlpha ? toRgbaString(draft.hex, draft.alpha) : draft.hex}
121 inputType={ChromeInputType.HEXA}
122 showAlpha={allowAlpha}
123 showEyeDropper
124 className="color-picker-chrome"
125 onChange={handleChange}
126 />
127 <style>{`
128 .color-picker-popover .color-picker-chrome {
129 width: 100% !important;
130 border-radius: 6px !important;
131 overflow: hidden;
132 --github-background-color: #fffdf8;
133 --github-border: 0;
134 --github-box-shadow: none;
135 --chrome-arrow-fill: #6f7d62;
136 --chrome-arrow-background-color: #f2ece0;
137 --editable-input-label-color: #7b735f;
138 --editable-input-color: #3e4a32;
139 --editable-input-box-shadow: #d7cbb7 0 0 0 1px inset;
140 }
141 .color-picker-popover .w-color-saturation {
142 border-radius: 5px 5px 0 0;
143 cursor: crosshair;
144 }
145 .color-picker-popover .w-color-hue,
146 .color-picker-popover .w-color-alpha {
147 cursor: ew-resize;
148 }
149 .color-picker-popover .w-color-chrome svg,
150 .color-picker-popover .w-color-chrome button,
151 .color-picker-popover .w-color-chrome input {
152 cursor: pointer;
153 }
154 .color-picker-popover .w-color-chrome input {
155 border-radius: 4px !important;
156 }
157 `}</style>
158 </PopoverContent>
159 </Popover>
160 </div>
161 )
162 }
163
163 lines Plain Text