From ab84b5f504dcbc7c47f7ac5ad89aee60ba93f47a 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: Fri, 13 Dec 2024 18:14:29 +0500
Subject: [PATCH] feat: finished - TreeList

---
 src/App.vue                                   |  71 ++++-
 src/assets/main.css                           |   3 +
 src/common/interfaces/components.ts           |  13 +-
 .../components/Button/Button.stories.ts       |   2 +-
 src/stories/components/Button/Button.vue      |  15 +-
 .../SelectButton.stories.ts}                  |   8 +-
 .../SelectButton.vue}                         |   3 +
 .../components/Slider/Slider.stories.ts       | 143 +++++++++++
 src/stories/components/Slider/Slider.vue      |  77 +++---
 src/stories/components/TreeList/TreeItems.vue | 154 +++++++++++
 .../components/TreeList/TreeList.stories.ts   |  85 +++++-
 src/stories/components/TreeList/TreeList.vue  | 243 ++----------------
 12 files changed, 538 insertions(+), 279 deletions(-)
 rename src/stories/components/{ToggleButton/ToggleButton.stories.ts => SelectButton/SelectButton.stories.ts} (93%)
 rename src/stories/components/{ToggleButton/ToggleButton.vue => SelectButton/SelectButton.vue} (98%)
 create mode 100644 src/stories/components/Slider/Slider.stories.ts
 create mode 100644 src/stories/components/TreeList/TreeItems.vue

diff --git a/src/App.vue b/src/App.vue
index 498670c..e2f6e63 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -103,9 +103,11 @@ import TableIcon from '@stories/icons/Mono/TableIcon.vue';
 import TrashIcon from '@stories/icons/Mono/TrashIcon.vue';
 import UserIcon from '@stories/icons/Mono/UserIcon.vue';
 import ArrowsVerticalIcon from '@stories/icons/Mono/ArrowsVerticalIcon.vue';
-import ToggleButton from '@stories/components/ToggleButton/ToggleButton.vue';
+import SelectButton from '@stories/components/SelectButton/SelectButton.vue';
 import ToggleSwitch from '@stories/components/ToggleSwitch/ToggleSwitch.vue';
 import TriangleIcon from '@stories/icons/Mono/TriangleIcon.vue';
+import Button from '@stories/components/Button/Button.vue';
+import Slider from '@stories/components/Slider/Slider.vue';
 
 const gentleIcons = [
   Age18Icon,
@@ -223,14 +225,77 @@ const options = [
     label: 'Second',
   },
 ];
+const sliderOptions = [
+  {
+    label: 0,
+    value: 0,
+    color: 'red',
+  },
+  {
+    label: 2,
+    value: 2,
+    color: 'orange',
+  },
+  {
+    label: 4,
+    value: 4,
+    color: 'yellow',
+  },
+  {
+    label: 6,
+    value: 6,
+    color: 'green',
+  },
+  {
+    label: 8,
+    value: 8,
+    color: 'sky',
+  },
+  {
+    label: 10,
+    value: 10,
+    color: 'purple',
+  },
+  {
+    label: 12,
+    value: 12,
+    color: 'purple',
+  },
+  {
+    label: 14,
+    value: 14,
+    color: 'purple',
+  },
+  {
+    label: 16,
+    value: 16,
+    color: 'purple',
+  },
+  {
+    label: 18,
+    value: 18,
+    color: 'purple',
+  },
+];
 </script>
 
 <template>
-  <ToggleButton :options="options" size="large">
+  <Slider
+    :options="sliderOptions"
+    width="400"
+    min="0"
+    max="18"
+    step="2"
+    backgroundColor="black"
+    theme="blue"
+    isSmooth
+  />
+  <Button theme="sky" label="I'm a button"></Button>
+  <SelectButton :options="options" size="large">
     <template #1Icon>
       <TrashIcon />
     </template>
-  </ToggleButton>
+  </SelectButton>
   <ToggleSwitch />
   <Drawer v-model:visible="visibleDrawer" theme="sky" closeIcon="CropIcon">
     <template #header>Это - Drawer</template>
diff --git a/src/assets/main.css b/src/assets/main.css
index 940084e..274ba60 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -148,6 +148,9 @@ body {
     min-height: 100vh;
     overflow-x: hidden;
 }
+* {
+    font-family: Arial, serif;
+}
 #app {
     min-height: 100vh;
     display: flex;
diff --git a/src/common/interfaces/components.ts b/src/common/interfaces/components.ts
index 8f276af..9c101ff 100644
--- a/src/common/interfaces/components.ts
+++ b/src/common/interfaces/components.ts
@@ -1,9 +1,14 @@
+import type { TIcons, TThemeColor } from '@interfaces/common';
+
 export interface ITreeItem {
   label: string;
   link?: string;
-  color?: string;
-  iconBefore?: string;
-  iconAfter?: string;
-  iconColor?: string;
+  linkBlank?: boolean;
+  color?: TThemeColor;
+  textStyle?: 'bold' | 'italic';
+  isTriangleToColor?: boolean;
+  iconBefore?: TIcons;
+  iconAfter?: TIcons;
+  iconColor?: TThemeColor;
   children?: ITreeItem[];
 }
diff --git a/src/stories/components/Button/Button.stories.ts b/src/stories/components/Button/Button.stories.ts
index 9d64adc..694b585 100644
--- a/src/stories/components/Button/Button.stories.ts
+++ b/src/stories/components/Button/Button.stories.ts
@@ -9,7 +9,7 @@ const meta: Meta = {
   parameters: {
     docs: {
       description: {
-        component: 'A component that is used to select a value from a list using a button.',
+        component: 'A component that is used as a button. Can be used with icon.',
       },
     },
   },
diff --git a/src/stories/components/Button/Button.vue b/src/stories/components/Button/Button.vue
index 63b95ba..fd21f80 100644
--- a/src/stories/components/Button/Button.vue
+++ b/src/stories/components/Button/Button.vue
@@ -6,13 +6,14 @@ import { convert500ThemeToColor } from '@helpers/colors';
 const props = withDefaults(
   defineProps<{
     label?: string;
-    size?: 'small' | 'medium' | 'large' | 'extraLarge';
+    size?: 'small' | 'medium' | 'large' | 'huge';
     textStyle?: 'bold' | 'italic';
     iconPos?: 'left' | 'top' | 'right' | 'bottom';
     width?: string | number;
     theme?: TThemeColor;
     textColor?: TThemeColor;
     border?: TThemeColor;
+    iconOnly?: boolean;
   }>(),
   {
     size: 'medium',
@@ -30,8 +31,8 @@ const textSize = computed(() => {
       return '12px';
     case 'large':
       return '20px';
-    case 'extraLarge':
-      return '24px';
+    case 'huge':
+      return '28px';
   }
   return '16px';
 });
@@ -41,7 +42,7 @@ const buttonPadding = computed(() => {
       return '0.5rem 0.375rem';
     case 'large':
       return '1.2rem 0.8rem';
-    case 'extraLarge':
+    case 'huge':
       return '1.8rem 1.2rem';
   }
   return '0.75rem 0.5rem';
@@ -62,6 +63,7 @@ const width = computed(() => (props.width ? `${props.width}px` : 'max-content'))
   >
     <span :style="`background-color: ${themeColor}`" class="background"></span>
     <span
+      v-if="label || !iconOnly"
       :style="`color: ${textColor}; font-size: ${textSize}`"
       :class="[
         'text',
@@ -70,7 +72,7 @@ const width = computed(() => (props.width ? `${props.width}px` : 'max-content'))
           italic: textStyle === 'italic',
         },
       ]"
-      >{{ label ?? 'Button' }}</span
+      >{{ label ? label : !iconOnly ? 'Button' : '' }}</span
     >
     <span
       v-if="$slots.default"
@@ -99,6 +101,9 @@ const width = computed(() => (props.width ? `${props.width}px` : 'max-content'))
 .button:hover .background {
   filter: brightness(90%);
 }
+.button:active .background {
+  filter: brightness(75%);
+}
 .background {
   width: 100%;
   height: 100%;
diff --git a/src/stories/components/ToggleButton/ToggleButton.stories.ts b/src/stories/components/SelectButton/SelectButton.stories.ts
similarity index 93%
rename from src/stories/components/ToggleButton/ToggleButton.stories.ts
rename to src/stories/components/SelectButton/SelectButton.stories.ts
index 64c15f2..9b19e46 100644
--- a/src/stories/components/ToggleButton/ToggleButton.stories.ts
+++ b/src/stories/components/SelectButton/SelectButton.stories.ts
@@ -1,11 +1,11 @@
 import type { Meta, StoryObj } from '@storybook/vue3';
 
-import ToggleButton from './ToggleButton.vue';
+import SelectButton from './SelectButton.vue';
 import { fn } from '@storybook/test';
 
 const meta: Meta = {
-  title: 'Components/ToggleButton',
-  component: ToggleButton,
+  title: 'Components/SelectButton',
+  component: SelectButton,
   tags: ['autodocs'],
   parameters: {
     docs: {
@@ -67,7 +67,7 @@ const meta: Meta = {
     // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
     onClick: fn(),
   },
-} satisfies Meta<typeof ToggleButton>;
+} satisfies Meta<typeof SelectButton>;
 
 export default meta;
 
diff --git a/src/stories/components/ToggleButton/ToggleButton.vue b/src/stories/components/SelectButton/SelectButton.vue
similarity index 98%
rename from src/stories/components/ToggleButton/ToggleButton.vue
rename to src/stories/components/SelectButton/SelectButton.vue
index 4367a1c..4a99fb8 100644
--- a/src/stories/components/ToggleButton/ToggleButton.vue
+++ b/src/stories/components/SelectButton/SelectButton.vue
@@ -152,6 +152,9 @@ const buttonHeight = computed(() => {
 .button:hover .background {
   filter: brightness(90%);
 }
+.button:active .background {
+  filter: brightness(75%);
+}
 .background {
   width: 100%;
   height: 100%;
diff --git a/src/stories/components/Slider/Slider.stories.ts b/src/stories/components/Slider/Slider.stories.ts
new file mode 100644
index 0000000..21452cc
--- /dev/null
+++ b/src/stories/components/Slider/Slider.stories.ts
@@ -0,0 +1,143 @@
+import type { Meta, StoryObj } from '@storybook/vue3';
+
+import Slider from './Slider.vue';
+import type { TThemeColor } from '@interfaces/common';
+
+const meta: Meta = {
+  title: 'Components/Slider',
+  component: Slider,
+  tags: ['autodocs'],
+  parameters: {
+    docs: {
+      description: {
+        component: 'A component to provide input with a drag handle.',
+      },
+    },
+  },
+  argTypes: {
+    options: { control: 'array' },
+    width: { control: 'text' },
+    min: { control: 'text' },
+    max: { control: 'text' },
+    step: { control: 'text' },
+    size: { control: 'select', options: ['small', 'medium', 'large', 'huge'] },
+    orientation: { control: 'select', options: ['horizontal', 'vertical'] },
+    isSmooth: { control: 'boolean' },
+    theme: {
+      control: 'select',
+      options: [
+        'white',
+        'slate',
+        'blue',
+        'sky',
+        'teal',
+        'green',
+        'yellow',
+        'orange',
+        'pink',
+        'fuchsia',
+        'purple',
+        'indigo',
+        'rose',
+        'red',
+        'black',
+      ],
+    },
+    backgroundColor: {
+      control: 'select',
+      options: [
+        'white',
+        'slate',
+        'blue',
+        'sky',
+        'teal',
+        'green',
+        'yellow',
+        'orange',
+        'pink',
+        'fuchsia',
+        'purple',
+        'indigo',
+        'rose',
+        'red',
+        'black',
+      ],
+    },
+  },
+  args: {
+    // primary: false,
+    // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
+  },
+} satisfies Meta<typeof Slider>;
+
+export default meta;
+
+type Story = StoryObj<typeof meta>;
+
+export const Primary: Story = {
+  args: {},
+};
+
+export const Full: Story = {
+  args: {
+    min: '0',
+    max: '10',
+    step: '2',
+    size: 'small',
+    backgroundColor: 'black',
+    theme: 'blue',
+    isSmooth: true,
+    options: [
+      {
+        label: 0,
+        value: 0,
+        color: 'red',
+      },
+      {
+        label: 2,
+        value: 2,
+        color: 'orange',
+      },
+      {
+        label: 4,
+        value: 4,
+        color: 'yellow',
+      },
+      {
+        label: 6,
+        value: 6,
+        color: 'green',
+      },
+      {
+        label: 8,
+        value: 8,
+        color: 'sky',
+      },
+      {
+        label: 10,
+        value: 10,
+        color: 'purple',
+      },
+      {
+        label: 12,
+        value: 12,
+        color: 'purple',
+      },
+      {
+        label: 14,
+        value: 14,
+        color: 'purple',
+      },
+      {
+        label: 16,
+        value: 16,
+        color: 'purple',
+      },
+      {
+        label: 18,
+        value: 18,
+        color: 'purple',
+      },
+    ],
+  },
+};
diff --git a/src/stories/components/Slider/Slider.vue b/src/stories/components/Slider/Slider.vue
index 6219be0..a689cbc 100644
--- a/src/stories/components/Slider/Slider.vue
+++ b/src/stories/components/Slider/Slider.vue
@@ -1,32 +1,37 @@
 <script setup lang="ts">
-import { useVModel } from '@vueuse/core';
-import { computed } from 'vue';
-import { convertThemeToColorBlackDefault, convertThemeToColorWhiteDefault } from './helpers/index';
+import { computed, ref, watch } from 'vue';
+import type { TThemeColor } from '@interfaces/common';
+import { convert500ThemeToColor } from '@helpers/colors';
 
-interface Props {
-  value: string | number;
-  width?: string | number;
-  min?: string | number;
-  max?: string | number;
-  step?: string | number;
-  size?: 'small' | 'medium' | 'large' | 'extraLarge';
-  theme?: string;
-  backgroundColor?: string;
-  orientation?: 'horizontal' | 'vertical';
-  isSmooth?: any;
-  options?: {
-    label: string;
-    value: number;
-    color?: string;
-  }[];
-}
-const props = defineProps<Props>();
-const emit = defineEmits(['update:value']);
-const value = useVModel(props, 'value', emit);
+const props = withDefaults(
+  defineProps<{
+    width?: string | number;
+    min?: string | number;
+    max?: string | number;
+    step?: string | number;
+    size?: 'small' | 'medium' | 'large' | 'huge';
+    theme?: TThemeColor;
+    backgroundColor?: TThemeColor;
+    orientation?: 'horizontal' | 'vertical';
+    isSmooth?: boolean;
+    options?: {
+      label: string;
+      value: number;
+      color?: string;
+    }[];
+  }>(),
+  {
+    width: '200',
+    size: 'medium',
+    theme: 'white',
+    backgroundColor: 'black',
+  },
+);
+const value = defineModel('value');
 const optionValue = ref(
   typeof value.value === 'string'
     ? props.options!.findIndex((option) => option.label === value.value)
-    : value.value
+    : value.value,
 );
 watch([optionValue], () => {
   if (props.options) {
@@ -42,23 +47,21 @@ watch([value], () => {
   }
 });
 const sliderButtonSize = computed(() => {
-  if (!props.size) return '40px';
   switch (props.size) {
     case 'small':
       return '25px';
-    case 'medium':
-      return '40px';
     case 'large':
       return '70px';
-    case 'extraLarge':
+    case 'huge':
       return '100px';
   }
+  return '40px';
 });
 const sliderHeight = computed(() => `${Math.floor(sliderButtonSize.value.slice(0, -2) / 3)}px`);
-const sliderBorderRadius = props.isSmooth ? sliderHeight.value : '0%';
-const sliderButtonBorderRadius = props.isSmooth ? '50%' : '0%';
-const themeColor = computed(() => convertThemeToColorWhiteDefault(props.theme));
-const themeBackground = computed(() => convertThemeToColorBlackDefault(props.backgroundColor));
+const sliderBorderRadius = computed(() => (props.isSmooth ? sliderHeight.value : '0%'));
+const sliderButtonBorderRadius = computed(() => (props.isSmooth ? '50%' : '0%'));
+const themeColor = computed(() => convert500ThemeToColor(props.theme));
+const themeBackground = computed(() => convert500ThemeToColor(props.backgroundColor));
 </script>
 
 <template>
@@ -66,10 +69,10 @@ const themeBackground = computed(() => convertThemeToColorBlackDefault(props.bac
     :class="[
       'slideContainer',
       {
-        verticalSlider: orientation === 'vertical'
-      }
+        verticalSlider: orientation === 'vertical',
+      },
     ]"
-    :style="`width: ${width ?? 200}px`"
+    :style="`width: ${width}px`"
   >
     <input
       v-model="optionValue"
@@ -90,8 +93,8 @@ const themeBackground = computed(() => convertThemeToColorBlackDefault(props.bac
         :class="[
           'values',
           {
-            datalistVertical: orientation === 'vertical'
-          }
+            datalistVertical: orientation === 'vertical',
+          },
         ]"
       >
         <template v-for="option of options" :key="option.value">
diff --git a/src/stories/components/TreeList/TreeItems.vue b/src/stories/components/TreeList/TreeItems.vue
new file mode 100644
index 0000000..68ebbe7
--- /dev/null
+++ b/src/stories/components/TreeList/TreeItems.vue
@@ -0,0 +1,154 @@
+<script setup lang="ts">
+import { iconsSet } from '@/common/constants/icons';
+import TriangleIcon from '@stories/icons/Mono/TriangleIcon.vue';
+import type { ITreeItem } from '@interfaces/components';
+import type { TThemeColor } from '@interfaces/common';
+import { convert500ThemeToColor } from '@helpers/colors';
+
+defineProps<{
+  state: {
+    isOpen: boolean;
+    label: string;
+  }[];
+  items: ITreeItem[];
+  textColor: TThemeColor;
+}>();
+const emit = defineEmits(['toggleIsOpen', 'onClick']);
+</script>
+
+<template>
+  <ul>
+    <li
+      v-for="item of items"
+      :key="item.label"
+      :class="[
+        'item',
+        {
+          pl27: !item.children,
+          openContent: state.find(
+            (itemState) => itemState.label === item.label && itemState.isOpen,
+          ),
+        },
+      ]"
+    >
+      <article>
+        <section
+          :class="[
+            'textContainer',
+            {
+              pointer: item.children,
+            },
+          ]"
+          @click="emit('toggleIsOpen', item)"
+        >
+          <TriangleIcon
+            v-if="item.children"
+            :class="[
+              'openButton',
+              {
+                openButtonOpened: state.find(
+                  (itemState) => itemState.label === item.label && itemState.isOpen,
+                ),
+              },
+            ]"
+            :color="
+              item.color && item.isTriangleToColor ? convert500ThemeToColor(item.color) : textColor
+            "
+            size="17"
+          />
+          <component
+            :is="iconsSet[item.iconBefore]"
+            v-if="item.iconBefore"
+            :color="convert500ThemeToColor(item.iconColor)"
+            style="min-width: 17px"
+            size="17"
+          />
+          <a
+            :href="item.link"
+            :target="item.linkBlank ? '_blank' : '_self'"
+            :class="[
+              'label',
+              {
+                bold: item.textStyle === 'bold',
+                italic: item.textStyle === 'italic',
+              },
+            ]"
+            :style="`color: ${item.color ? convert500ThemeToColor(item.color) : textColor}`"
+            @click="emit('onClick', item.link)"
+            >{{ item.label }}</a
+          >
+          <component
+            :is="iconsSet[item.iconAfter]"
+            v-if="item.iconAfter"
+            :color="convert500ThemeToColor(item.iconColor)"
+            style="min-width: 17px"
+            size="17"
+          />
+        </section>
+        <section class="children">
+          <TreeItems
+            :items="item.children"
+            :state="state"
+            :textColor="textColor"
+            @toggleIsOpen="emit('toggleIsOpen', $event)"
+          />
+        </section>
+      </article>
+    </li>
+  </ul>
+</template>
+
+<style scoped>
+.item {
+  width: 100%;
+  transition: all 0.4s ease;
+  background-color: v-bind(themeColor);
+}
+.label {
+  display: inline-block;
+  position: relative;
+  padding: 4px 5px;
+  background-color: v-bind(themeColor);
+  word-break: break-word;
+}
+.openButton {
+  margin-right: 10px;
+  min-width: 17px;
+  transition: transform 0.3s ease;
+}
+.openButtonOpened {
+  transform: rotate(180deg);
+}
+.children {
+  width: 100%;
+  padding-left: 15px;
+  display: grid;
+  grid-template-rows: 0fr;
+  transition:
+    all 0.3s ease-in,
+    grid-template-rows 0.3s ease-out;
+}
+.children > ul {
+  overflow: hidden;
+}
+.openContent > article > .children {
+  grid-template-rows: 1fr;
+  opacity: 1;
+}
+.textContainer {
+  position: relative;
+  display: flex;
+}
+.pointer {
+  cursor: pointer;
+}
+.pl27 {
+  padding-left: 27px;
+}
+.bold {
+  font-weight: bold;
+}
+.italic {
+  font-style: italic;
+}
+</style>
diff --git a/src/stories/components/TreeList/TreeList.stories.ts b/src/stories/components/TreeList/TreeList.stories.ts
index 9a0d9e4..5ccd861 100644
--- a/src/stories/components/TreeList/TreeList.stories.ts
+++ b/src/stories/components/TreeList/TreeList.stories.ts
@@ -9,7 +9,8 @@ const meta: Meta = {
   parameters: {
     docs: {
       description: {
-        component: 'A component that is used to select a value from a list using a TreeList.',
+        component:
+          'A component that is used to display hierarchical data. Can contain links to a new page or the same.',
       },
     },
   },
@@ -17,7 +18,6 @@ const meta: Meta = {
     items: { control: 'array' },
     maxWidth: { control: 'number' },
     expand: { control: 'boolean' },
-    disabled: { control: 'boolean' },
     theme: {
       control: 'select',
       options: [
@@ -60,6 +60,24 @@ export const Primary: Story = {
             children: [
               {
                 label: '1-1-1',
+                children: [
+                  {
+                    label: '1-1-1-1',
+                    children: [
+                      {
+                        label: '1-1-1-1-1',
+                        children: [
+                          { label: '1-1-1-1-1-1' },
+                          { label: '1-1-1-1-1-2' },
+                          { label: '1-1-1-1-1-3' },
+                          { label: '1-1-1-1-1-4' },
+                        ],
+                      },
+                    ],
+                  },
+                  { label: '1-1-1-2' },
+                  { label: '1-1-1-3' },
+                ],
               },
               {
                 label: '1-1-2',
@@ -84,5 +102,66 @@ export const Primary: Story = {
 };
 
 export const Full: Story = {
-  args: {},
+  args: {
+    items: [
+      {
+        label: 'Font-family (mdn web docs)',
+        color: 'green',
+        iconBefore: 'EncyclopediaIcon',
+        iconColor: 'green',
+        link: 'https://developer.mozilla.org/en-US/docs/Web/CSS/font-family',
+        linkBlank: true,
+        textStyle: 'bold',
+        children: [
+          {
+            label: 'Font-family (mdn web docs) but same page (error?)',
+            link: 'https://developer.mozilla.org/en-US/docs/Web/CSS/font-family',
+            color: 'sky',
+            iconBefore: 'EyeIcon',
+            iconColor: 'white',
+            textStyle: 'italic',
+            isTriangleToColor: true,
+            children: [
+              {
+                label: '1-1-1',
+                color: 'red',
+                iconAfter: 'DiceIcon',
+                iconColor: 'red',
+                children: [
+                  {
+                    label: '1-1-1-1',
+                    children: [
+                      { label: '1-1-1-1-1' },
+                      { label: '1-1-1-1-2' },
+                      { label: '1-1-1-1-3' },
+                      { label: '1-1-1-1-4' },
+                    ],
+                  },
+                  { label: '1-1-1-2' },
+                  { label: '1-1-1-3' },
+                ],
+              },
+              {
+                label: '1-1-2',
+              },
+            ],
+          },
+          {
+            label: '1-2',
+          },
+        ],
+      },
+      {
+        label: 'Second',
+        children: [
+          {
+            label: '2-1',
+          },
+        ],
+      },
+    ],
+
+    expand: true,
+    theme: 'black',
+  },
 };
diff --git a/src/stories/components/TreeList/TreeList.vue b/src/stories/components/TreeList/TreeList.vue
index bc6f912..d13489d 100644
--- a/src/stories/components/TreeList/TreeList.vue
+++ b/src/stories/components/TreeList/TreeList.vue
@@ -2,9 +2,8 @@
 import { computed, ref, watch } from 'vue';
 import type { ITreeItem } from '@interfaces/components';
 import type { TThemeColor } from '@interfaces/common';
-import { iconsSet } from '@/common/constants/icons';
 import { convert500ThemeToColor } from '@helpers/colors';
-import TriangleIcon from '@stories/icons/Mono/TriangleIcon.vue';
+import TreeItems from '@stories/components/TreeList/TreeItems.vue';
 
 const props = withDefaults(
   defineProps<{
@@ -12,12 +11,10 @@ const props = withDefaults(
     maxWidth?: number;
     expand?: boolean;
     theme?: TThemeColor;
-    disabled?: boolean;
   }>(),
   {
     theme: 'white',
     maxWidth: 300,
-    disabled: false,
   },
 );
 const emit = defineEmits(['onClick']);
@@ -30,31 +27,21 @@ const textColor = computed(() => {
 });
 
 const state = ref([]);
-const setInitialState = () => {
-  if (!props?.items) return;
-  for (const item of props.items) {
+const setItemChildrenToState = (items: ITreeItem[]) => {
+  for (const item of items) {
     state.value.push({
       isOpen: props?.expand,
       label: item.label,
     });
     if (item.children) {
-      for (const child of item.children) {
-        state.value.push({
-          isOpen: props?.expand,
-          label: child.label,
-        });
-        if (child.children) {
-          for (const grandChild of child.children) {
-            state.value.push({
-              isOpen: props?.expand,
-              label: grandChild.label,
-            });
-          }
-        }
-      }
+      setItemChildrenToState(item.children);
     }
   }
 };
+const setInitialState = () => {
+  if (!props?.items) return;
+  setItemChildrenToState(props.items);
+};
 watch(
   [items],
   () => {
@@ -71,206 +58,18 @@ const toggleIsOpen = (item) =>
 </script>
 
 <template>
-  <ul :style="`background-color: ${themeColor ?? 'white'}; max-width: ${maxWidth}px`" class="tree">
-    <li
-      v-for="item of items"
-      :key="item.label"
-      :class="[
-        'item',
-        {
-          openContent: state.find(
-            (itemState) => itemState.label === item.label && itemState.isOpen,
-          ),
-        },
-      ]"
-    >
-      <section
-        :class="[
-          'textContainer',
-          {
-            pointer: !disabled && item.children,
-          },
-        ]"
-      >
-        <TriangleIcon
-          v-if="item.children && !disabled"
-          :class="[
-            'openButton',
-            {
-              openButtonOpened: state.find(
-                (itemState) => itemState.label === item.label && itemState.isOpen,
-              ),
-            },
-          ]"
-          :color="textColor"
-          size="15"
-          @click.prevent="toggleIsOpen(item)"
-        />
-        <component
-          :is="iconsSet[item.iconBefore]"
-          v-if="item.iconBefore"
-          :color="item.iconColor"
-          size="20"
-        />
-        <a :href="item.link" class="label" @click.prevent="emit('onClick', item.link)">{{
-          item.label
-        }}</a>
-        <component
-          :is="iconsSet[item.iconAfter]"
-          v-if="item.iconAfter"
-          :color="item.iconColor"
-          size="20"
-        />
-      </section>
-      <div class="children">
-        <div
-          v-for="child of item.children"
-          :key="child.label"
-          :class="[
-            'item',
-            {
-              pl50: !child.children,
-              openContent: state.find(
-                (itemState) => itemState.label === child.label && itemState.isOpen,
-              ),
-            },
-          ]"
-        >
-          <section
-            :class="[
-              'textContainer',
-              {
-                pointer: !disabled && child.children,
-              },
-            ]"
-          >
-            <TriangleIcon
-              v-if="child.children && !disabled"
-              :class="[
-                'openButton',
-                {
-                  openButtonOpened: state.find(
-                    (itemState) => itemState.label === child.label && itemState.isOpen,
-                  ),
-                },
-              ]"
-              :color="textColor"
-              size="15"
-              @click.prevent="toggleIsOpen(child)"
-            />
-            <component
-              :is="iconsSet[child.iconBefore]"
-              v-if="child.iconBefore"
-              :color="child.iconColor"
-              size="20"
-            />
-            <a :href="child.link" class="label" @click.prevent="emit('onClick', item.link)">{{
-              child.label
-            }}</a>
-            <component
-              :is="iconsSet[child.iconAfter]"
-              v-if="child.iconAfter"
-              :color="child.iconColor"
-              size="20"
-            />
-          </section>
-          <div class="children">
-            <div
-              v-for="grandChild of child.children"
-              :key="grandChild.label"
-              :class="[
-                'item',
-                {
-                  pl50: !grandChild.children,
-                  openContent: state.find(
-                    (itemState) => itemState.label === grandChild.label && itemState.isOpen,
-                  ),
-                },
-              ]"
-            >
-              <div class="textContainer">
-                <component
-                  :is="iconsSet[grandChild.iconBefore]"
-                  v-if="grandChild.iconBefore"
-                  :color="grandChild.iconColor"
-                  size="20"
-                />
-                <p
-                  :href="grandChild.link"
-                  class="label"
-                  @click.prevent="emit('onClick', item.link)"
-                >
-                  {{ grandChild.label }}
-                </p>
-                <component
-                  :is="iconsSet[grandChild.iconAfter]"
-                  v-if="grandChild.iconAfter"
-                  :color="grandChild.iconColor"
-                  size="20"
-                />
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </li>
-  </ul>
+  <div
+    :style="`background-color: ${themeColor ?? 'white'}; max-width: ${maxWidth}px; padding: 15px 25px 15px 15px`"
+    class="tree"
+  >
+    <TreeItems
+      :items="items"
+      :state="state"
+      :textColor="textColor"
+      @toggleIsOpen="toggleIsOpen"
+      @onClick="emit('onClick')"
+    />
+  </div>
 </template>
 
-<style scoped>
-.tree {
-  padding: 15px 25px 15px 15px;
-}
-.item {
-  width: 100%;
-  padding-left: 20px;
-  overflow: hidden;
-  transition: all 0.3s ease;
-  background-color: v-bind(themeColor);
-}
-.label {
-  display: inline-block;
-  position: relative;
-  padding: 4px 0;
-  z-index: 3;
-  color: v-bind(textColor);
-  background-color: v-bind(themeColor);
-  word-break: break-word;
-}
-.openButton {
-  margin-right: 15px;
-  transition: transform 0.3s ease;
-}
-.openButtonOpened {
-  transform: rotate(180deg);
-}
-.children {
-  width: 100%;
-  padding-left: 25px;
-  opacity: 0;
-  max-height: 0;
-  transform: translateY(-100%);
-  transition: all 0.3s ease;
-}
-.openContent > .children {
-  transform: translateY(0);
-  opacity: 1;
-  max-height: 1000px;
-}
-.textContainer {
-  position: relative;
-  display: flex;
-  gap: 10px;
-}
-.flex {
-  display: flex;
-  align-items: start;
-  justify-content: end;
-}
-.pointer {
-  cursor: pointer;
-}
-.pl50 {
-  padding-left: 50px;
-}
-</style>
+<style scoped></style>
-- 
GitLab