ColorPicker Ultra
ColorPickerWithGradient.vue
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { twMerge } from 'tailwind-merge'
import { createReusableTemplate } from '@vueuse/core'
import { SliderRoot, SliderThumb, SliderTrack } from 'reka-ui'
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
import { ColorPickerInputHex, ColorPickerInputHSL, ColorPickerInputRGB, ColorPickerInputHSB } from '@vuelor/picker'
import { ColorPickerRoot, ColorPickerCanvas, ColorPickerEyeDropper, ColorPickerSwatch } from '@vuelor/picker'
import { ColorPickerSliderHue, ColorPickerSliderAlpha } from '@vuelor/picker'
import { HexaToRGBA, RGBAtoHexa } from '@vuelor/picker'
import Select from '../common/Select.vue'
import GradientStopInput from '../common/GradientStopInput.vue'
const [DefineColorPickerTemplate, ColorPicker] = createReusableTemplate()
const INPUTS = {
Hex: ColorPickerInputHex,
RGB: ColorPickerInputRGB,
HSL: ColorPickerInputHSL,
HSB: ColorPickerInputHSB
}
type ModelValue = string | null
interface Props {
class?: string
disabled?: boolean
modelValue?: ModelValue
}
const props = withDefaults(defineProps<Props>(), {
disabled: false,
modelValue: null
})
const emit = defineEmits<{
(e: 'update:modelValue', value: ModelValue): void
}>()
const format = ref<'Hex' | 'RGB' | 'HSL' | 'HSB'>('Hex')
const formatOptions = ['Hex', 'RGB', 'HSL', 'HSB']
const swatches = ref<string[]>([
'#00C3D0FF',
'#00C8B3FF',
'#34C759FF',
'#FFCC00FF',
'#FF383CFF',
'#FF8D2825',
'#FF383C40',
'#FF8D2880',
'#FFCC0080',
'#34C759FF',
'#00C8B3FF',
'#00C3D0FF',
'#0088FFFF',
'#6155F5FF',
'#CB30E0FF',
'#FF2D55FF',
'#FF2D5525',
'#AC7F5EFF'
])
const canvasType = computed<'HSL' | 'HSV'>(() => {
return format.value === 'HSL' ? 'HSL' : 'HSV'
})
const color = ref<string | null>(null)
const mode = ref<'color' | 'gradient'>('color')
const gradientType = ref('Linear')
const gradientTypeOptions = ['Linear', 'Radial', 'Conic']
const gradientAngle = ref(90)
const gradientStops = ref([0, 33, 66, 100])
const gradientSelectedStopIndex = ref(0)
const gradientColors = ref(['#FF98C2FF', '#4DC1FFFF', '#D082E8FF', '#FFFA7AFF'])
const currentColor = computed<ModelValue>({
get: () => {
return mode.value === 'color'
? color.value
: gradientColors.value[gradientSelectedStopIndex.value]
},
set: (value: ModelValue) => {
if (mode.value === 'color') {
color.value = value as string
} else {
gradientColors.value[gradientSelectedStopIndex.value] = value as string
}
}
})
function addStop () {
const lastIndex = gradientStops.value.length - 1
const prevIndex = gradientStops.value.length > 1 ? gradientStops.value.length - 2 : 0
const newStop = gradientStops.value.length > 1
? ((gradientStops.value[lastIndex] + gradientStops.value[prevIndex]) / 2)
: gradientStops.value[lastIndex] <= 50 ? 100 : 0
const colorA = HexaToRGBA(gradientColors.value[prevIndex])
const colorB = HexaToRGBA(gradientColors.value[lastIndex])
const newColor = RGBAtoHexa({
r: (colorA.r + colorB.r) / 2,
g: (colorA.g + colorB.g) / 2,
b: (colorA.b + colorB.b) / 2,
a: (colorA.a + colorB.a) / 2
})
const insertIndex = (gradientStops.value.length === 1) ? 1 : lastIndex
gradientStops.value.splice(insertIndex, 0, Math.round(newStop))
gradientStops.value = gradientStops.value.sort((a, b) => a - b)
gradientColors.value.splice(insertIndex, 0, newColor)
};
function removeStop (index: number) {
if (gradientStops.value.length < 2) return
gradientSelectedStopIndex.value = index > 0 ? index - 1 : 0
gradientStops.value.splice(index, 1)
gradientColors.value.splice(index, 1)
}
function handleSelectStop (index: number) {
gradientSelectedStopIndex.value = index
}
function handleReverseGradient () {
gradientColors.value.reverse()
}
function handleRotateGradient () {
gradientAngle.value = (gradientAngle.value + 90) % 360
}
const gradientStopsList = computed(() => {
return gradientStops.value.map((value, index) => `${gradientColors.value[index]} ${value}%`).join(', ')
})
const trackBackground = computed(() => {
return `linear-gradient(to right, ${gradientStopsList.value})`
})
const modelValue = computed(() => {
if (mode.value === 'color') {
return color.value
}
switch (gradientType.value) {
case 'Radial':
return `radial-gradient(circle at center, ${gradientStopsList.value})`
case 'Conic':
return `conic-gradient(from ${gradientAngle.value}deg, ${gradientStopsList.value})`
case 'Linear':
default:
return `linear-gradient(${gradientAngle.value}deg, ${gradientStopsList.value})`
}
})
watch(
modelValue,
(newValue) => emit('update:modelValue', newValue),
{ immediate: true }
)
</script>
<template>
<ColorPickerRoot
v-model="currentColor"
class="block p-0"
:class="props.class"
:disabled="props.disabled"
:ui="{ input: { label: 'hidden', field: 'max-w-12' } }"
>
<DefineColorPickerTemplate>
<div class="p-4 flex flex-col gap-2">
<ColorPickerCanvas :type="canvasType" />
<div class="flex items-center gap-3">
<ColorPickerEyeDropper>
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M17.52 6.471a1.62 1.62 0 0 0-2.295.003l-1.87 1.88-.354.355-.355-.354-.01-.01a.9.9 0 0 0-1.272 0l-.02.02a.9.9 0 0 0 0 1.273l.51.51 2 2 .51.51a.9.9 0 0 0 1.272 0l.02-.02a.9.9 0 0 0 0-1.273l-.01-.01-.352-.353.351-.353 1.879-1.888a1.62 1.62 0 0 0-.003-2.29m-3.004-.702a2.621 2.621 0 1 1 3.717 3.697l-1.57 1.579a1.9 1.9 0 0 1-.3 2.3l-.02.02a1.9 1.9 0 0 1-2.687 0l-.156-.157-5.647 5.642a.5.5 0 0 1-.353.147H5.504a.5.5 0 0 1-.5-.5L5 16.503a.5.5 0 0 1 .146-.354l5.647-5.647-.157-.156a1.9 1.9 0 0 1 0-2.687l.02-.02a1.9 1.9 0 0 1 2.299-.3zm-3.016 5.44 1.293 1.292-5.5 5.496h-1.29L6 16.707z"
/>
</svg>
</ColorPickerEyeDropper>
<div class="flex flex-col flex-1 gap-2">
<ColorPickerSliderHue />
<ColorPickerSliderAlpha />
</div>
</div>
<div class="flex items-center gap-2">
<Select
v-model="format"
class="w-[56px]"
label="Color format"
placeholder="Format"
:disabled="props.disabled"
:options="formatOptions"
/>
<component :is="INPUTS[format]" />
</div>
</div>
<div class="border-t px-3 py-2 grid grid-cols-9">
<ColorPickerSwatch
v-for="color in swatches"
:value="color"
class="m-1"
@click="currentColor = color"
/>
</div>
</DefineColorPickerTemplate>
<TabsRoot
v-model="mode"
default-value="color"
>
<div class="flex justify-between p-2 border-b">
<TabsList class="flex gap-1">
<TabsTrigger class="h-6 w-6 rounded-sm data-[state=active]:bg-[#f5f5f5]" value="color">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="#0000004d" d="M9 9h6v6H9z" />
<path fill="#000000e6" fill-rule="evenodd" clip-rule="evenodd" d="M8 7h8a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1M6 8a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2zm3 7V9h6v6zM8 8.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5z" />
</svg>
</TabsTrigger>
<TabsTrigger class="h-6 w-6 rounded-sm data-[state=active]:bg-[#f5f5f5]" value="gradient">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="#000000e6" fill-rule="evenodd" clip-rule="evenodd" d="M8 7h8a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1M6 8a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2zm3.75.875a.875.875 0 1 1-1.75 0 .875.875 0 0 1 1.75 0m3.791.625a.625.625 0 1 0 0-1.25.625.625 0 0 0 0 1.25m-1.458.875a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m0 3.12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.458 2.245a.625.625 0 1 0 0-1.25.625.625 0 0 0 0 1.25m.625-3.865a.625.625 0 1 1-1.25 0 .625.625 0 0 1 1.25 0M8.875 15.99a.875.875 0 1 0 0-1.75.875.875 0 0 0 0 1.75m.875-4.115a.875.875 0 1 1-1.75 0 .875.875 0 0 1 1.75 0m5.75-1a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1m.5 2.623a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0" />
</svg>
</TabsTrigger>
</TabsList>
<button class="h-6 w-6 rounded-[5px] hover:bg-[#f5f5f5] focus:outline focus:outline-[#0d99ff]">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="#000000e6" d="M16.224 7.082a.501.501 0 0 1 .694.693l-.065.078L12.707 12l4.146 4.146.064.078a.5.5 0 0 1-.693.694l-.078-.065L12 12.706l-4.147 4.147a.5.5 0 1 1-.707-.707l4.147-4.147-4.147-4.146-.064-.078a.501.501 0 0 1 .693-.693l.078.064L12 11.293l4.146-4.147z" />
</svg>
</button>
</div>
<TabsContent value="color">
<ColorPicker />
</TabsContent>
<TabsContent class="pb-3" value="gradient">
<div class="h-12 pl-4 pr-2 flex items-center justify-between gap-2">
<Select
v-model="gradientType"
class="w-24"
label="Gradient type"
placeholder="Type"
:disabled="props.disabled"
:options="gradientTypeOptions"
/>
<div class="flex items-center gap-1">
<button
:disabled="gradientStops.length === 1"
class="rounded-[5px] enabled:hover:bg-[#f5f5f5] disabled:opacity-50 focus:outline focus:outline-[#0d99ff]"
@click="handleReverseGradient"
>
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M8.354 6.354a.5.5 0 1 0-.708-.708l-2.5 2.5a.5.5 0 0 0 0 .708l2.5 2.5a.5.5 0 0 0 .708-.708L6.707 9H18.5a.5.5 0 0 0 0-1H6.707zm7.292 7a.5.5 0 0 1 .708-.708l2.5 2.5a.5.5 0 0 1 0 .708l-2.5 2.5a.5.5 0 0 1-.708-.708L17.293 16H5.5a.5.5 0 0 1 0-1h11.793z" />
</svg>
</button>
<button
:disabled="gradientType === 'Radial'"
class="rounded-[5px] enabled:hover:bg-[#f5f5f5] disabled:opacity-50 focus:outline focus:outline-[#0d99ff]"
@click="handleRotateGradient"
>
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" fill-rule="evenodd" d="M10.233 6.474a2.5 2.5 0 0 1 3.535 0L15.293 8H14a.5.5 0 0 0 0 1h2.5a.5.5 0 0 0 .5-.5V6a.5.5 0 1 0-1 0v1.292l-1.525-1.525a3.5 3.5 0 0 0-4.95 0L7.147 8.146a.5.5 0 0 0 .707.707zm2.828 3.172a1.5 1.5 0 0 0-2.121 0l-3.293 3.293a1.5 1.5 0 0 0 0 2.121l3.293 3.293a1.5 1.5 0 0 0 2.12 0l3.294-3.293a1.5 1.5 0 0 0 0-2.121zm-1.414.707a.5.5 0 0 1 .707 0l3.293 3.293a.5.5 0 0 1 0 .707l-3.293 3.293a.5.5 0 0 1-.707 0l-3.293-3.293a.5.5 0 0 1 0-.707z" />
</svg>
</button>
</div>
</div>
<div class="pt-4 px-4">
<SliderRoot
v-model="gradientStops"
class="relative flex items-center select-none touch-none"
thumbAlignment="overflow"
>
<SliderTrack
:style="{ background: trackBackground }"
class="relative grow rounded-[5px] h-8 shadow-vuelor-inner"
/>
<SliderThumb
v-for="(_, i) in gradientStops.length"
aria-label="Stop"
:class="twMerge(['flex items-center justify-center w-6 h-6 -mt-8 bg-white drop-shadow-vuelor-thumb rounded-[5px] focus:outline-none relative after:content-[\'\'] after:absolute after:top-[100%] after:left-1/2 after:-translate-x-1/2 after:border-l-[6px] after:border-l-transparent after:border-r-[6px] after:border-r-transparent after:border-t-[6px] after:border-t-white', gradientSelectedStopIndex === i ? 'bg-[#0d99ff] after:border-t-[#0d99ff]' : ''])"
@pointerdown="handleSelectStop(i)"
>
<ColorPickerSwatch
as="span"
class="w-3.5 h-3.5 border border-[#0000001a] rounded-sm"
:value="gradientColors[i]"
/>
</SliderThumb>
</SliderRoot>
</div>
<div class="h-8 pl-4 pr-2 mt-2 mb-1 flex items-center justify-between">
<span class="text-black text-[11px] font-bold">Stops</span>
<button
:disabled="gradientStops.length > 7"
class="rounded-[5px] enabled:hover:bg-[#f5f5f5] disabled:opacity-50 focus:outline focus:outline-[#0d99ff]"
@click="addStop"
>
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 6a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 12 6"
/>
</svg>
</button>
</div>
<div
v-for="(_, index) in gradientStops"
:class="{ 'bg-[#e5f4ff]': gradientSelectedStopIndex === index }"
class="h-8 pl-4 pr-2 flex items-center gap-2"
@mousedown="handleSelectStop(index)"
>
<GradientStopInput v-model="gradientStops[index]" />
<ColorPickerInputHex class="flex-1" v-model="gradientColors[index]">
<template #before>
<PopoverRoot>
<PopoverTrigger as-child>
<ColorPickerSwatch
:value="gradientColors[index]"
@click="gradientSelectedStopIndex = index"
/>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
side="left"
align="start"
:alignOffset="-100"
:sideOffset="75"
data-vuelor-docs
class="bg-white w-60 z-10 rounded-lg shadow-vuelor-card"
>
<ColorPicker />
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
</ColorPickerInputHex>
<button
class="rounded-[5px] hover:bg-[#f5f5f5] focus:outline focus:outline-[#0d99ff]"
:style="{ visibility: gradientStops.length < 2 ? 'hidden' : undefined }"
@click="removeStop(index)"
@pointerdown.prevent
>
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
d="M6 12a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 6 12"
/>
</svg>
</button>
</div>
</TabsContent>
</TabsRoot>
</ColorPickerRoot>
</template>