From ab5c73d98b8b51b332cde5b62adc5ee6249fc075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9C=D0=B0?= =?UTF-8?q?=D0=BB=D1=8E=D0=B3=D0=B8=D0=BD?= <d.malygin@iqdev.digital> Date: Mon, 3 Feb 2025 14:03:16 +0500 Subject: [PATCH] feat: 'ColorPicker' --- src/common/interfaces/componentsProps.ts | 7 + src/components/Carousel/Carousel.stories.ts | 2 +- src/components/ColorPicker/Button.vue | 94 +++++++++++ .../ColorPicker/ColorPicker.stories.ts | 43 +++++ src/components/ColorPicker/ColorPicker.vue | 149 ++++++++++++++++++ 5 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 src/components/ColorPicker/Button.vue create mode 100644 src/components/ColorPicker/ColorPicker.stories.ts create mode 100644 src/components/ColorPicker/ColorPicker.vue diff --git a/src/common/interfaces/componentsProps.ts b/src/common/interfaces/componentsProps.ts index b81ef92..13d4a5e 100644 --- a/src/common/interfaces/componentsProps.ts +++ b/src/common/interfaces/componentsProps.ts @@ -182,6 +182,13 @@ export interface IPopupProps { left?: number; } +export interface IColorPickerProps { + size?: TSize; + disabled?: boolean; + buttonProps?: IButtonProps; + sameButtonColor?: boolean; +} + export interface ISelectProps { options?: ISelectOption[]; groups?: ISelectGroup[]; diff --git a/src/components/Carousel/Carousel.stories.ts b/src/components/Carousel/Carousel.stories.ts index 40940ea..a5b7c85 100644 --- a/src/components/Carousel/Carousel.stories.ts +++ b/src/components/Carousel/Carousel.stories.ts @@ -9,7 +9,7 @@ const meta: Meta = { parameters: { docs: { description: { - component: 'A component to define number inputs with a dial.', + component: 'A content slider.', }, }, }, diff --git a/src/components/ColorPicker/Button.vue b/src/components/ColorPicker/Button.vue new file mode 100644 index 0000000..3769148 --- /dev/null +++ b/src/components/ColorPicker/Button.vue @@ -0,0 +1,94 @@ +<script setup lang="ts"> +import { computed } from 'vue'; +import type { IButtonProps } from '@interfaces/componentsProps'; +import { convertThemeToSecondaryColor, convertThemeToColor } from '@helpers/common'; + +interface Props extends IButtonProps { + disabled?: boolean; + color: string | null; +} +const props = withDefaults(defineProps<Props>(), { + size: 'normal', + theme: 'white', + iconPos: 'left', + darknessTheme: '500', + darknessTextColor: '500', +}); + +const themeColor = computed(() => props.color ?? convertThemeToColor(props.theme, props.darknessTheme)); +const borderColor = computed(() => + props.disabled ? '#62708c' : convertThemeToSecondaryColor(props.theme, props.darknessTheme), +); +const width = computed(() => (props.width ? props.width : 'max-content')); +</script> + +<template> + <div + :class="[ + 'button', + { + 'flex-column': iconPos === 'top' || iconPos === 'bottom', + border: borderColor, + }, + ]" + :style="`width: ${width}`" + > + <span :style="`background-color: ${themeColor}`" class="background"></span> + <span + v-if="$slots.default" + :class="[ + 'icon', + { + 'order-1': iconPos === 'left' || iconPos === 'top', + }, + ]" + > + <slot /> + </span> + </div> +</template> + +<style scoped> +.button { + position: relative; + border-radius: 7px; + display: inline-flex; + justify-content: center; + align-items: center; + user-select: none; + transition: filter 0.2s ease-in-out; +} +.button:hover { + filter: brightness(90%); +} +.button:active { + filter: brightness(75%); +} +.background { + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + border-radius: 5px; +} +.text { + position: relative; + z-index: 2; + line-height: 1; +} +.icon { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; +} +.order-1 { + order: -1; +} +.border { + border: 2px solid v-bind(borderColor); +} +</style> diff --git a/src/components/ColorPicker/ColorPicker.stories.ts b/src/components/ColorPicker/ColorPicker.stories.ts new file mode 100644 index 0000000..cf71cf7 --- /dev/null +++ b/src/components/ColorPicker/ColorPicker.stories.ts @@ -0,0 +1,43 @@ +import type { Meta, StoryObj } from '@storybook/vue3'; + +import ColorPicker from './ColorPicker.vue'; + +const meta: Meta = { + title: 'Components/ColorPicker', + component: ColorPicker, + tags: ['pick'], + parameters: { + docs: { + description: { + component: 'A component to define number inputs with a dial.', + }, + }, + }, + argTypes: { + buttonProps: { control: 'object' }, + sameButtonColor: { control: 'boolean' }, + disabled: { control: 'boolean' }, + size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, + }, +} satisfies Meta<typeof ColorPicker>; + +export default meta; + +type Story = StoryObj<typeof meta>; + +export const Simple: Story = { + args: {}, +}; + +export const Full: Story = { + args: { + buttonProps: { + label: 'Pick color!', + theme: 'red', + textStyle: 'bold', + }, + + size: 'large', + sameButtonColor: true, + }, +}; diff --git a/src/components/ColorPicker/ColorPicker.vue b/src/components/ColorPicker/ColorPicker.vue new file mode 100644 index 0000000..c492514 --- /dev/null +++ b/src/components/ColorPicker/ColorPicker.vue @@ -0,0 +1,149 @@ +<script setup lang="ts"> +import type { IColorPickerProps } from '@interfaces/componentsProps'; +import { computed, type Ref } from 'vue'; +import Button from './Button.vue'; +import { convertThemeToColor, convertThemeToTextColor } from '@helpers/common'; + +const props = withDefaults(defineProps<IColorPickerProps>(), { + size: 'normal', + disabled: false, +}); + +const value = defineModel() as Ref<string>; +const size = computed(() => { + const size = props.size; + if (size === 'normal') return '25px'; + if (size === 'large') return '40px'; + if (size === 'huge') return '60px'; + return '15px'; +}); +const borderWidth = computed(() => (props.size === 'small' ? '2px' : '3px')); +const borderRadius = computed(() => `calc(${size.value} * 0.3)`); + +function wc_hex_is_light(color) { + if (!color) return true; + const hex = color.replace('#', ''); + const c_r = parseInt(hex.substring(0, 2), 16); + const c_g = parseInt(hex.substring(2, 4), 16); + const c_b = parseInt(hex.substring(4, 6), 16); + const brightness = (c_r * 299 + c_g * 587 + c_b * 114) / 1000; + return brightness > 150; +} + +const color = computed(() => + props.buttonProps.textColor + ? convertThemeToColor(props.buttonProps.textColor, props.buttonProps.darknessTextColor) + : props.sameButtonColor + ? !wc_hex_is_light(value?.value) + ? 'white' + : 'black' + : convertThemeToTextColor(props.buttonProps.theme ?? 'white', props.buttonProps.darknessTheme), +); +const textSize = computed(() => { + switch (props.size) { + case 'small': + return '12px'; + case 'large': + return '20px'; + case 'huge': + return '28px'; + } + return '16px'; +}); +const buttonPadding = computed(() => { + if (props.buttonProps.padding) return props.buttonProps.padding; + switch (props.size) { + case 'small': + return '0.5rem'; + case 'large': + return '1.2rem'; + case 'huge': + return '1.8rem'; + } + return '0.75rem'; +}); +</script> + +<template> + <div class="container"> + <Button + v-if="buttonProps" + v-bind="buttonProps" + :color="sameButtonColor ? value : null" + :class="{ + disabledButton: disabled, + }" + :disabled="disabled" + > + <label + for="inputColor" + :style="`padding: ${buttonPadding}; color: ${color}; font-size: ${textSize}`" + :class="[ + 'text', + { + bold: buttonProps?.textStyle === 'bold', + italic: buttonProps?.textStyle === 'italic', + }, + ]" + >{{ buttonProps?.label ?? 'Button' }}</label + > + </Button> + <input + type="color" + id="inputColor" + v-model="value" + :disabled="disabled" + :class="{ + noVisible: buttonProps, + disabled, + }" + /> + </div> +</template> + +<style scoped> +.container { + position: relative; +} +.noVisible { + opacity: 0; + position: absolute; + bottom: 0; + left: 0; +} +.text { + border-radius: 5px; +} +.disabledButton { + * { + background-color: #e1e7f1 !important; + color: #62708c !important; + } + pointer-events: none; +} +.disabled { + border-color: #62708c !important; + cursor: auto; +} +input { + position: absolute; + z-index: -1; + -webkit-appearance: none; + appearance: none; + border: v-bind(borderWidth) solid black; + border-radius: v-bind(borderRadius); + outline: none; + width: v-bind(size); + height: v-bind(size); + padding: 0; + background-color: transparent; + cursor: pointer; +} +input[type='color']::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type='color']::-webkit-color-swatch { + border: none; +} +</style> -- GitLab