Skip to content

Commit c6cd9d1

Browse files
committed
feat: improve color picker
1 parent 04b7093 commit c6cd9d1

File tree

1 file changed

+129
-2
lines changed

1 file changed

+129
-2
lines changed

frontend/app/components/admin/ColorPickerInput.vue

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,142 @@
11
<script setup lang="ts">
22
const color = defineModel<string>({ required: true })
3+
4+
type ColorFormat = 'hex' | 'rgba'
5+
6+
const format = ref<ColorFormat>(color.value.startsWith('rgb') ? 'rgba' : 'hex')
7+
8+
// Parse color to extract RGB and alpha
9+
function parseColor(colorStr: string): {
10+
r: number
11+
g: number
12+
b: number
13+
a: number
14+
} {
15+
// Modern format: rgb(r g b / a) or rgba(r g b / a)
16+
const modernMatch = colorStr.match(
17+
/rgba?\((\d+)\s+(\d+)\s+(\d+)(?:\s*\/\s*([\d.]+))?\)/,
18+
)
19+
if (modernMatch) {
20+
return {
21+
r: parseInt(modernMatch[1]!),
22+
g: parseInt(modernMatch[2]!),
23+
b: parseInt(modernMatch[3]!),
24+
a: modernMatch[4] ? parseFloat(modernMatch[4]) : 1,
25+
}
26+
}
27+
28+
// Legacy format: rgba(r, g, b, a) or rgb(r, g, b)
29+
const legacyMatch = colorStr.match(
30+
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/,
31+
)
32+
if (legacyMatch) {
33+
return {
34+
r: parseInt(legacyMatch[1]!),
35+
g: parseInt(legacyMatch[2]!),
36+
b: parseInt(legacyMatch[3]!),
37+
a: legacyMatch[4] ? parseFloat(legacyMatch[4]) : 1,
38+
}
39+
}
40+
41+
let hex = colorStr.replace('#', '')
42+
if (hex.length === 3) {
43+
hex = hex
44+
.split('')
45+
.map((c) => c + c)
46+
.join('')
47+
}
48+
if (hex.length === 6) {
49+
hex += 'ff'
50+
}
51+
52+
return {
53+
r: parseInt(hex.slice(0, 2), 16) || 0,
54+
g: parseInt(hex.slice(2, 4), 16) || 0,
55+
b: parseInt(hex.slice(4, 6), 16) || 0,
56+
a: (parseInt(hex.slice(6, 8), 16) || 255) / 255,
57+
}
58+
}
59+
60+
function toHex(r: number, g: number, b: number): string {
61+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
62+
}
63+
64+
function toRgb(r: number, g: number, b: number, a: number): string {
65+
return `rgb(${r} ${g} ${b} / ${a.toFixed(2)})`
66+
}
67+
68+
const hexColor = computed({
69+
get() {
70+
const { r, g, b } = parseColor(color.value)
71+
return toHex(r, g, b)
72+
},
73+
set(newHex: string) {
74+
const { a } = parseColor(color.value)
75+
const hex = newHex.replace('#', '')
76+
const r = parseInt(hex.slice(0, 2), 16) || 0
77+
const g = parseInt(hex.slice(2, 4), 16) || 0
78+
const b = parseInt(hex.slice(4, 6), 16) || 0
79+
color.value = format.value === 'rgba' ? toRgb(r, g, b, a) : toHex(r, g, b)
80+
},
81+
})
82+
83+
const opacity = computed({
84+
get() {
85+
const { a } = parseColor(color.value)
86+
return Math.round(a * 100)
87+
},
88+
set(newOpacity: number) {
89+
const { r, g, b } = parseColor(color.value)
90+
color.value = toRgb(r, g, b, newOpacity / 100)
91+
},
92+
})
93+
94+
function switchFormat(newFormat: ColorFormat) {
95+
format.value = newFormat
96+
const { r, g, b, a } = parseColor(color.value)
97+
if (newFormat === 'hex') {
98+
color.value = toHex(r, g, b)
99+
} else {
100+
color.value = toRgb(r, g, b, a)
101+
}
102+
}
103+
3104
const chip = computed(() => ({ backgroundColor: color.value }))
4105
</script>
5106

6107
<template>
7108
<UInput v-model="color">
8109
<template #leading>
9110
<UPopover :ui="{ content: 'p-1' }">
10-
<button :style="chip" class="size-4 rounded" />
111+
<button :style="chip" class="border-accented size-4 rounded border" />
11112
<template #content>
12-
<UColorPicker v-model="color" class="p-2" />
113+
<div class="flex flex-col gap-2 p-2">
114+
<UColorPicker v-model="hexColor" />
115+
<div class="flex items-center gap-2">
116+
<UTabs
117+
:items="[
118+
{ label: 'HEX', value: 'hex' },
119+
{ label: 'RGBA', value: 'rgba' },
120+
]"
121+
:model-value="format"
122+
size="xs"
123+
class="flex-1"
124+
@update:model-value="switchFormat($event as ColorFormat)"
125+
/>
126+
</div>
127+
<div v-if="format === 'rgba'" class="flex items-center gap-2">
128+
<USlider
129+
v-model="opacity"
130+
:min="0"
131+
:max="100"
132+
:step="1"
133+
class="flex-1"
134+
/>
135+
<span class="text-muted w-8 text-right text-xs"
136+
>{{ opacity }}%</span
137+
>
138+
</div>
139+
</div>
13140
</template>
14141
</UPopover>
15142
</template>

0 commit comments

Comments
 (0)