From 145551ba1fa9e069916ccd8457a37de7b5763768 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: Thu, 30 Jan 2025 17:09:48 +0500 Subject: [PATCH] feat: 'Carousel' in process ("size", "buttonsBelow" and "theme" are remaining) --- src/Playground.vue | 27 ++++ src/common/interfaces/componentsProps.ts | 4 +- src/components/Carousel/Carousel.stories.ts | 122 ++++++++++-------- src/components/Carousel/Carousel.vue | 84 ++++++++++-- .../Carousel/CarouselArrowContainer.vue | 50 +++---- src/components/Carousel/helpers.ts | 32 +++++ 6 files changed, 222 insertions(+), 97 deletions(-) create mode 100644 src/components/Carousel/helpers.ts diff --git a/src/Playground.vue b/src/Playground.vue index a101e80..b62d5b2 100644 --- a/src/Playground.vue +++ b/src/Playground.vue @@ -21,6 +21,7 @@ import Knob from '@components/Knob/Knob.vue'; import Rating from '@components/Rating/Rating.vue'; import HomeIcon from '@icons/Mono/HomeIcon.vue'; import ProgressBar from '@components/ProgressBar/ProgressBar.vue'; +import Carousel from '@components/Carousel/Carousel.vue'; const visibleDrawer = ref(false); const sliderOptions: ISliderOptions[] = [ @@ -201,6 +202,31 @@ const openDrawer = () => (visibleDrawer.value = true); <Checkbox v-model="activeCheckbox" size="large" /> <Checkbox v-model="activeCheckbox" size="huge" /> <ProgressBar v-model="pbValue" /> + <Carousel + style="margin: 20px" + :itemsProps="[ + { + index: 1, + text: 'This is SPARTA!', + }, + { + index: 2, + text: 'This is the second item!', + }, + { + index: 3, + text: + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi atque blanditiis debitis distinctio, doloribus,\n' + + ' eius est eveniet excepturi facere id iure laboriosam laborum libero, minus nesciunt nostrum nulla repellat\n' + + ' veritatis.', + }, + ]" + > + <template v-slot="item: unknown"> + <h2 style="text-align: center; margin-bottom: 20px">Element {{ item?.index }}</h2> + <p>{{ item?.text }}</p></template + > + </Carousel> {{ tableData[1] }} <Table center @@ -211,6 +237,7 @@ const openDrawer = () => (visibleDrawer.value = true); theme="black" stripedRows paginator + editable :no-editing-settings="{ cells: [[0, 0]], }" diff --git a/src/common/interfaces/componentsProps.ts b/src/common/interfaces/componentsProps.ts index 829d2a3..730438a 100644 --- a/src/common/interfaces/componentsProps.ts +++ b/src/common/interfaces/componentsProps.ts @@ -81,10 +81,12 @@ export interface IPaginatorProps { export interface ICarouselProps { itemsProps: unknown[]; - width?: string; + innerWidth?: string; size?: TSize; perView?: number; perScroll?: number; + circular?: boolean; + buttonsBelow?: boolean; theme?: TThemeColor; darknessTheme?: TDarkness; } diff --git a/src/components/Carousel/Carousel.stories.ts b/src/components/Carousel/Carousel.stories.ts index a69e2a2..e23bf89 100644 --- a/src/components/Carousel/Carousel.stories.ts +++ b/src/components/Carousel/Carousel.stories.ts @@ -14,25 +14,14 @@ const meta: Meta = { }, }, argTypes: { - buttons: { control: 'boolean' }, - showLabel: { control: 'boolean' }, - colorAsTheme: { control: 'boolean' }, - textBold: { control: 'boolean' }, - min: { control: 'number' }, - max: { control: 'number' }, - step: { control: 'number' }, - fontSize: { control: 'text' }, - textBefore: { control: 'text' }, - textAfter: { control: 'text' }, - colorGaps: { control: 'object' }, + itemsProps: { control: 'object' }, + innerWidth: { control: 'text' }, + perView: { control: 'number' }, + perScroll: { control: 'number' }, + circular: { control: 'boolean' }, + buttonsBelow: { control: 'boolean' }, size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, - background: { control: 'color' }, darknessTheme: { control: 'select', options: ['100', '200', '300', '400', '500', '600', '700', '800', '900'] }, - darknessNegativeTheme: { - control: 'select', - options: ['100', '200', '300', '400', '500', '600', '700', '800', '900'], - }, - darknessColor: { control: 'select', options: ['100', '200', '300', '400', '500', '600', '700', '800', '900'] }, theme: { control: 'select', options: [ @@ -53,46 +42,6 @@ const meta: Meta = { 'black', ], }, - negativeTheme: { - control: 'select', - options: [ - 'white', - 'blue', - 'sky', - 'cyan', - 'teal', - 'green', - 'yellow', - 'orange', - 'pink', - 'fuchsia', - 'purple', - 'indigo', - 'rose', - 'red', - 'black', - ], - }, - color: { - control: 'select', - options: [ - 'white', - 'blue', - 'sky', - 'cyan', - 'teal', - 'green', - 'yellow', - 'orange', - 'pink', - 'fuchsia', - 'purple', - 'indigo', - 'rose', - 'red', - 'black', - ], - }, }, } satisfies Meta<typeof Carousel>; @@ -103,3 +52,62 @@ type Story = StoryObj<typeof meta>; export const Simple: Story = { args: {}, }; + +export const Half: Story = { + args: { + circular: true, + perView: 2, + perScroll: 1, + itemsProps: [ + { + header: 'First', + text: 'Some text', + }, + { + header: 'Second', + text: 'Some text', + }, + { + header: 'Third', + text: 'Some text', + }, + { + header: 'Forth', + text: 'Some text', + }, + ], + }, +}; + +export const Full: Story = { + args: { + circular: true, + perView: 2, + perScroll: 2, + + itemsProps: [ + { + header: 'First', + text: 'Some text', + }, + { + header: 'Second', + text: 'Some text', + }, + { + header: 'Third', + text: 'Some text', + }, + { + header: 'Forth', + text: 'Some text', + }, + { + header: 'Fifth', + text: 'Some text', + }, + ], + + buttonsBelow: true, + }, +}; diff --git a/src/components/Carousel/Carousel.vue b/src/components/Carousel/Carousel.vue index e9310a7..ae856fb 100644 --- a/src/components/Carousel/Carousel.vue +++ b/src/components/Carousel/Carousel.vue @@ -5,19 +5,26 @@ import { computed, ref } from 'vue'; import { convertThemeToColor, convertThemeToTextColor } from '@helpers/common'; import ArrowLeftShortIcon from '@icons/Mono/ArrowLeftShortIcon.vue'; import ArrowRightShortIcon from '@icons/Mono/ArrowRightShortIcon.vue'; +import { defaultProps, getNewValue } from './helpers'; const props = withDefaults(defineProps<ICarouselProps>(), { - itemsProps: () => [], size: 'normal', + innerWidth: '300px', + perView: 1, + perScroll: 1, + theme: 'white', + darknessTheme: '500', }); const current = ref(1); -const itemsLength = computed(() => Math.ceil(props.itemsProps.length / props.perView)); +const itemsLength = computed(() => props.itemsProps?.length ?? 3); const color = computed(() => convertThemeToColor(props.theme, props.darknessTheme)); const textColor = computed(() => convertThemeToTextColor(props.theme, props.darknessTheme)); -const isStartDisabled = computed(() => current.value === 1); -const isEndDisabled = computed(() => current.value === itemsLength.value); +const isStartDisabled = computed(() => (props.circular ? false : current.value === 1 || itemsLength.value <= 1)); +const isEndDisabled = computed(() => + props.circular ? false : current.value === Math.ceil(itemsLength.value / props.perView) || !itemsLength.value, +); const iconSize = computed(() => { const size = props.size; if (size === 'normal') return '10'; @@ -25,22 +32,83 @@ const iconSize = computed(() => { if (size === 'huge') return '18'; return '7'; }); +const itemWidth = computed(() => `calc(${props.innerWidth} / ${props.perView}`); +const translate = computed(() => `translateX(calc(-${props.innerWidth} / ${props.perView} * ${current.value - 1}))`); </script> <template> - <section :style="`width: ${width}; min-height: 100px`" class="carouselContainer"> - <CarouselArrowContainer :textColor="textColor" :color="color" :disable="isStartDisabled"> + <section class="carouselContainer"> + <CarouselArrowContainer + @click="!isStartDisabled ? (current = getNewValue('-', current, itemsLength, perScroll, perView)) : null" + :textColor="textColor" + :color="color" + :disable="isStartDisabled" + > <ArrowLeftShortIcon :color="isStartDisabled ? '#aaa' : textColor" :size="iconSize" /> </CarouselArrowContainer> - <slot /> - <CarouselArrowContainer :textColor="textColor" :color="color" :disable="isEndDisabled"> + <div class="content"> + <ul class="list"> + <li v-for="item of Array(itemsLength).keys()" :key="item" class="item"> + <slot v-bind="itemsProps?.[item]" :key="current - 1" /> + <div v-if="!$slots.default && !itemsProps"> + <h2 style="text-align: center; margin-bottom: 10px">{{ defaultProps[item].header }}</h2> + <p> + {{ defaultProps[item].text }} + </p> + </div> + <div v-else-if="!$slots.default"> + <h2 style="text-align: center; margin-bottom: 10px">{{ itemsProps[item].header }}</h2> + <p> + {{ itemsProps[item].text }} + </p> + </div> + </li> + </ul> + </div> + <CarouselArrowContainer + @click="!isEndDisabled ? (current = getNewValue('+', current, itemsLength, perScroll, perView)) : null" + :textColor="textColor" + :color="color" + :disable="isEndDisabled" + > <ArrowRightShortIcon :color="isEndDisabled ? '#aaa' : textColor" :size="iconSize" /> </CarouselArrowContainer> + <ul class="buttons"> + <li + v-for="item of Array(itemsLength).keys()" + :key="item" + class="button" + :style="`width: ${iconSize}px; height: ${iconSize}px`" + ></li> + </ul> </section> </template> <style scoped> .carouselContainer { display: flex; + min-height: 100px; + position: relative; +} +.content { + max-width: v-bind(innerWidth); + overflow: hidden; +} +.list { + display: flex; + transform: v-bind(translate); + transition: transform 0.3s ease-out; +} +.item { + min-width: v-bind(itemWidth); +} +.buttons { + position: absolute; + bottom: 0; + left: 0; + transform: translateX(-50%); +} +.button { + background-color: v-bind(color); } </style> diff --git a/src/components/Carousel/CarouselArrowContainer.vue b/src/components/Carousel/CarouselArrowContainer.vue index 7dbf50e..4e15837 100644 --- a/src/components/Carousel/CarouselArrowContainer.vue +++ b/src/components/Carousel/CarouselArrowContainer.vue @@ -7,23 +7,23 @@ defineProps<{ </script> <template> - <div class="arrowContainer"> + <div + :class="[ + 'arrowContainer', + { + disable, + }, + ]" + > <div :class="[ - 'icon', + 'bg', { - disable, + disableBg: disable, }, ]" - > - <div - :class="[ - 'bg', - { - disableBg: disable, - }, - ]" - ></div> + ></div> + <div class="icon"> <slot /> </div> </div> @@ -31,49 +31,37 @@ defineProps<{ <style scoped> .arrowContainer { - width: 50px; + position: relative; + min-width: 50px; min-height: 100%; display: flex; justify-content: center; align-items: center; + cursor: pointer; } .icon { - position: relative; display: flex; justify-content: center; align-items: center; - cursor: pointer; line-height: 1.2; color: v-bind(textColor); } -.icon::before { - content: ''; - position: absolute; - width: 100%; - height: 100%; - border-radius: 50%; - z-index: -1; - background-color: v-bind(color); -} -.icon:hover > .bg { +.arrowContainer:hover > .bg { background-color: v-bind(textColor); opacity: 0.1; } -.icon:active > .bg { +.arrowContainer:active > .bg { opacity: 0.2; } .bg { width: 100%; height: 100%; position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); padding: 10px; z-index: 5; - border-radius: 50%; - background-color: transparent; opacity: 0; + border-radius: 5px; + background-color: transparent; transition: all 0.2s ease; } .disable { diff --git a/src/components/Carousel/helpers.ts b/src/components/Carousel/helpers.ts new file mode 100644 index 0000000..81d1016 --- /dev/null +++ b/src/components/Carousel/helpers.ts @@ -0,0 +1,32 @@ +export const getNewValue = ( + action: '+' | '-', + current: number, + itemsLength: number, + perScroll: number, + perView: number, +): number => { + const additional = perView - 1; + if (action === '+') { + if (current + additional === itemsLength) return 1; + if (current + perScroll >= itemsLength) return itemsLength - additional; + return current + perScroll; + } + if (current === 1) return itemsLength - additional; + if (current - perScroll < 1) return 1; + return current - perScroll; +}; + +export const defaultProps = [ + { + header: 'Element â„–1', + text: 'This is the first element.', + }, + { + header: 'Element â„–2', + text: 'Oh...one more!', + }, + { + header: 'Element â„–3', + text: 'I feel sick.....', + }, +]; -- GitLab