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