mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 06:29:30 -07:00
Added <LogoLoop /> Animation
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// Highlighted sidebar items
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel', 'Scroll Stack', 'Faulty Terminal', 'Pill Nav', 'Card Nav'];
|
||||
export const NEW = ['Target Cursor', 'Ripple Grid', 'Magic Bento', 'Galaxy', 'Text Type', 'Glass Surface', 'Sticker Peel', 'Scroll Stack', 'Faulty Terminal', 'Pill Nav', 'Card Nav', 'Logo Loop'];
|
||||
export const UPDATED = [];
|
||||
|
||||
// Used for main sidebar navigation
|
||||
@@ -38,6 +38,7 @@ export const CATEGORIES = [
|
||||
'Fade Content',
|
||||
'Noise',
|
||||
'Splash Cursor',
|
||||
'Logo Loop',
|
||||
'Pixel Transition',
|
||||
'Target Cursor',
|
||||
'Sticker Peel',
|
||||
|
||||
@@ -3,6 +3,7 @@ const animations = {
|
||||
'animated-content': () => import('../demo/Animations/AnimatedContentDemo.vue'),
|
||||
'pixel-transition': () => import('../demo/Animations/PixelTransitionDemo.vue'),
|
||||
'glare-hover': () => import('../demo/Animations/GlareHoverDemo.vue'),
|
||||
'logo-loop': () => import('../demo/Animations/LogoLoopDemo.vue'),
|
||||
'magnet-lines': () => import('../demo/Animations/MagnetLinesDemo.vue'),
|
||||
'click-spark': () => import('../demo/Animations/ClickSparkDemo.vue'),
|
||||
'ribbons': () => import('../demo/Animations/RibbonsDemo.vue'),
|
||||
|
||||
55
src/constants/code/Animations/logoLoopCode.ts
Normal file
55
src/constants/code/Animations/logoLoopCode.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import code from '@/content/Animations/LogoLoop/LogoLoop.vue?raw';
|
||||
import { createCodeObject } from '@/types/code';
|
||||
|
||||
export const logoLoop = createCodeObject(code, 'Animations/LogoLoop', {
|
||||
usage: `<template>
|
||||
<div :style="{ height: '200px', position: 'relative', overflow: 'hidden' }">
|
||||
<LogoLoop
|
||||
:logos="techLogos"
|
||||
:speed="120"
|
||||
direction="left"
|
||||
:logoHeight="48"
|
||||
:gap="40"
|
||||
:pauseOnHover="true"
|
||||
:scaleOnHover="true"
|
||||
:fadeOut="true"
|
||||
fadeOutColor="#ffffff"
|
||||
ariaLabel="Technology partners"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LogoLoop from './LogoLoop.vue';
|
||||
|
||||
const techLogos = [
|
||||
{
|
||||
node: '<i class="pi pi-code" style="font-size: 2rem;"></i>',
|
||||
title: "Development",
|
||||
href: "https://vuejs.org"
|
||||
},
|
||||
{
|
||||
node: '<i class="pi pi-desktop" style="font-size: 2rem;"></i>',
|
||||
title: "Frontend",
|
||||
href: "https://vitejs.dev"
|
||||
},
|
||||
{
|
||||
node: '<i class="pi pi-server" style="font-size: 2rem;"></i>',
|
||||
title: "Backend",
|
||||
href: "https://nodejs.org"
|
||||
},
|
||||
{
|
||||
node: '<i class="pi pi-database" style="font-size: 2rem;"></i>',
|
||||
title: "Database",
|
||||
href: "https://www.postgresql.org"
|
||||
},
|
||||
];
|
||||
|
||||
// Alternative with image sources
|
||||
const imageLogos = [
|
||||
{ src: "/logos/company1.png", alt: "Company 1", href: "https://company1.com" },
|
||||
{ src: "/logos/company2.png", alt: "Company 2", href: "https://company2.com" },
|
||||
{ src: "/logos/company3.png", alt: "Company 3", href: "https://company3.com" },
|
||||
];
|
||||
</script>`
|
||||
});
|
||||
436
src/content/Animations/LogoLoop/LogoLoop.vue
Normal file
436
src/content/Animations/LogoLoop/LogoLoop.vue
Normal file
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="rootClasses"
|
||||
:style="containerStyle"
|
||||
role="region"
|
||||
:aria-label="ariaLabel"
|
||||
@mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<template v-if="fadeOut">
|
||||
<div
|
||||
aria-hidden
|
||||
:class="[
|
||||
'pointer-events-none absolute inset-y-0 left-0 z-[1]',
|
||||
'w-[clamp(24px,8%,120px)]',
|
||||
'bg-[linear-gradient(to_right,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]'
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
:class="[
|
||||
'pointer-events-none absolute inset-y-0 right-0 z-[1]',
|
||||
'w-[clamp(24px,8%,120px)]',
|
||||
'bg-[linear-gradient(to_left,var(--logoloop-fadeColor,var(--logoloop-fadeColorAuto))_0%,rgba(0,0,0,0)_100%)]'
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div ref="trackRef" :class="['flex w-max will-change-transform select-none', 'motion-reduce:transform-none']">
|
||||
<ul
|
||||
v-for="copyIndex in copyCount"
|
||||
:key="`copy-${copyIndex - 1}`"
|
||||
class="flex items-center"
|
||||
role="list"
|
||||
:aria-hidden="copyIndex > 1"
|
||||
:ref="
|
||||
el => {
|
||||
if (copyIndex === 1) seqRef = el as HTMLUListElement;
|
||||
}
|
||||
"
|
||||
>
|
||||
<li
|
||||
v-for="(item, itemIndex) in logos"
|
||||
:key="`${copyIndex - 1}-${itemIndex}`"
|
||||
:class="[
|
||||
'flex-none mr-[var(--logoloop-gap)] text-[length:var(--logoloop-logoHeight)] leading-[1]',
|
||||
scaleOnHover && 'overflow-visible group/item'
|
||||
]"
|
||||
role="listitem"
|
||||
>
|
||||
<a
|
||||
v-if="item.href"
|
||||
:class="[
|
||||
'inline-flex items-center no-underline rounded',
|
||||
'transition-opacity duration-200 ease-linear',
|
||||
'hover:opacity-80',
|
||||
'focus-visible:outline focus-visible:outline-current focus-visible:outline-offset-2'
|
||||
]"
|
||||
:href="item.href"
|
||||
:aria-label="getItemAriaLabel(item) || 'logo link'"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<LogoContent :item="item" :scale-on-hover="scaleOnHover" />
|
||||
</a>
|
||||
<LogoContent v-else :item="item" :scale-on-hover="scaleOnHover" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
|
||||
|
||||
export type LogoItemNode = {
|
||||
node: string;
|
||||
href?: string;
|
||||
title?: string;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export type LogoItemImage = {
|
||||
src: string;
|
||||
alt?: string;
|
||||
href?: string;
|
||||
title?: string;
|
||||
srcSet?: string;
|
||||
sizes?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export type LogoItem = LogoItemNode | LogoItemImage;
|
||||
|
||||
export interface LogoLoopProps {
|
||||
logos: LogoItem[];
|
||||
speed?: number;
|
||||
direction?: 'left' | 'right';
|
||||
width?: number | string;
|
||||
logoHeight?: number;
|
||||
gap?: number;
|
||||
pauseOnHover?: boolean;
|
||||
fadeOut?: boolean;
|
||||
fadeOutColor?: string;
|
||||
scaleOnHover?: boolean;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
const ANIMATION_CONFIG = {
|
||||
SMOOTH_TAU: 0.25,
|
||||
MIN_COPIES: 2,
|
||||
COPY_HEADROOM: 2
|
||||
} as const;
|
||||
|
||||
const props = withDefaults(defineProps<LogoLoopProps>(), {
|
||||
speed: 120,
|
||||
direction: 'left',
|
||||
width: '100%',
|
||||
logoHeight: 60,
|
||||
gap: 32,
|
||||
pauseOnHover: true,
|
||||
fadeOut: false,
|
||||
scaleOnHover: false,
|
||||
ariaLabel: 'Partner logos'
|
||||
});
|
||||
|
||||
const containerRef = useTemplateRef('containerRef');
|
||||
const trackRef = useTemplateRef('trackRef');
|
||||
const seqRef = ref<HTMLUListElement | null>(null);
|
||||
|
||||
const seqWidth = ref<number>(0);
|
||||
const copyCount = ref<number>(ANIMATION_CONFIG.MIN_COPIES);
|
||||
const isHovered = ref<boolean>(false);
|
||||
|
||||
let rafRef: number | null = null;
|
||||
let lastTimestampRef: number | null = null;
|
||||
const offsetRef = ref(0);
|
||||
const velocityRef = ref(0);
|
||||
|
||||
const targetVelocity = computed(() => {
|
||||
const magnitude = Math.abs(props.speed);
|
||||
const directionMultiplier = props.direction === 'left' ? 1 : -1;
|
||||
const speedMultiplier = props.speed < 0 ? -1 : 1;
|
||||
return magnitude * directionMultiplier * speedMultiplier;
|
||||
});
|
||||
|
||||
const cssVariables = computed(() => ({
|
||||
'--logoloop-gap': `${props.gap}px`,
|
||||
'--logoloop-logoHeight': `${props.logoHeight}px`,
|
||||
...(props.fadeOutColor && { '--logoloop-fadeColor': props.fadeOutColor })
|
||||
}));
|
||||
|
||||
const rootClasses = computed(() => {
|
||||
const classes = [
|
||||
'relative overflow-x-hidden group',
|
||||
'[--logoloop-gap:32px]',
|
||||
'[--logoloop-logoHeight:28px]',
|
||||
'[--logoloop-fadeColorAuto:#ffffff]',
|
||||
'dark:[--logoloop-fadeColorAuto:#0b0b0b]'
|
||||
];
|
||||
|
||||
if (props.scaleOnHover) {
|
||||
classes.push('py-[calc(var(--logoloop-logoHeight)*0.1)]');
|
||||
}
|
||||
|
||||
if (props.className) {
|
||||
classes.push(props.className);
|
||||
}
|
||||
|
||||
return classes;
|
||||
});
|
||||
|
||||
const containerStyle = computed(() => ({
|
||||
width: typeof props.width === 'number' ? `${props.width}px` : props.width,
|
||||
...cssVariables.value,
|
||||
...(typeof props.style === 'object' && props.style !== null ? props.style : {})
|
||||
}));
|
||||
|
||||
const isNodeItem = (item: LogoItem): item is LogoItemNode => 'node' in item;
|
||||
|
||||
const getItemAriaLabel = (item: LogoItem): string | undefined => {
|
||||
if (isNodeItem(item)) {
|
||||
return item.ariaLabel ?? item.title;
|
||||
}
|
||||
return item.alt ?? item.title;
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (props.pauseOnHover) {
|
||||
isHovered.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (props.pauseOnHover) {
|
||||
isHovered.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateDimensions = async () => {
|
||||
await nextTick();
|
||||
const containerWidth = containerRef.value?.clientWidth ?? 0;
|
||||
const sequenceWidth = seqRef.value?.getBoundingClientRect?.()?.width ?? 0;
|
||||
|
||||
if (sequenceWidth > 0) {
|
||||
seqWidth.value = Math.ceil(sequenceWidth);
|
||||
const copiesNeeded = Math.ceil(containerWidth / sequenceWidth) + ANIMATION_CONFIG.COPY_HEADROOM;
|
||||
copyCount.value = Math.max(ANIMATION_CONFIG.MIN_COPIES, copiesNeeded);
|
||||
|
||||
cleanupAnimation?.();
|
||||
cleanupAnimation = startAnimationLoop();
|
||||
}
|
||||
};
|
||||
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
const setupResizeObserver = () => {
|
||||
if (!window.ResizeObserver) {
|
||||
const handleResize = () => updateDimensions();
|
||||
window.addEventListener('resize', handleResize);
|
||||
updateDimensions();
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}
|
||||
|
||||
resizeObserver = new ResizeObserver(updateDimensions);
|
||||
|
||||
if (containerRef.value) {
|
||||
resizeObserver.observe(containerRef.value);
|
||||
}
|
||||
if (seqRef.value) {
|
||||
resizeObserver.observe(seqRef.value);
|
||||
}
|
||||
|
||||
updateDimensions();
|
||||
|
||||
return () => {
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
};
|
||||
};
|
||||
|
||||
const setupImageLoader = () => {
|
||||
const images = seqRef.value?.querySelectorAll('img') ?? [];
|
||||
|
||||
if (images.length === 0) {
|
||||
updateDimensions();
|
||||
return;
|
||||
}
|
||||
|
||||
let remainingImages = images.length;
|
||||
const handleImageLoad = () => {
|
||||
remainingImages -= 1;
|
||||
if (remainingImages === 0) {
|
||||
updateDimensions();
|
||||
}
|
||||
};
|
||||
|
||||
images.forEach(img => {
|
||||
const htmlImg = img as HTMLImageElement;
|
||||
if (htmlImg.complete) {
|
||||
handleImageLoad();
|
||||
} else {
|
||||
htmlImg.addEventListener('load', handleImageLoad, { once: true });
|
||||
htmlImg.addEventListener('error', handleImageLoad, { once: true });
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
images.forEach(img => {
|
||||
img.removeEventListener('load', handleImageLoad);
|
||||
img.removeEventListener('error', handleImageLoad);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const startAnimationLoop = () => {
|
||||
const track = trackRef.value;
|
||||
if (!track) return;
|
||||
|
||||
const prefersReduced =
|
||||
typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
if (seqWidth.value > 0) {
|
||||
offsetRef.value = ((offsetRef.value % seqWidth.value) + seqWidth.value) % seqWidth.value;
|
||||
track.style.transform = `translate3d(${-offsetRef.value}px, 0, 0)`;
|
||||
}
|
||||
|
||||
if (prefersReduced) {
|
||||
track.style.transform = 'translate3d(0, 0, 0)';
|
||||
return () => {
|
||||
lastTimestampRef = null;
|
||||
};
|
||||
}
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (lastTimestampRef === null) {
|
||||
lastTimestampRef = timestamp;
|
||||
}
|
||||
|
||||
const deltaTime = Math.max(0, timestamp - lastTimestampRef) / 1000;
|
||||
lastTimestampRef = timestamp;
|
||||
|
||||
const target = props.pauseOnHover && isHovered.value ? 0 : targetVelocity.value;
|
||||
|
||||
const easingFactor = 1 - Math.exp(-deltaTime / ANIMATION_CONFIG.SMOOTH_TAU);
|
||||
velocityRef.value += (target - velocityRef.value) * easingFactor;
|
||||
|
||||
if (seqWidth.value > 0) {
|
||||
let nextOffset = offsetRef.value + velocityRef.value * deltaTime;
|
||||
nextOffset = ((nextOffset % seqWidth.value) + seqWidth.value) % seqWidth.value;
|
||||
offsetRef.value = nextOffset;
|
||||
|
||||
const translateX = -offsetRef.value;
|
||||
track.style.transform = `translate3d(${translateX}px, 0, 0)`;
|
||||
}
|
||||
|
||||
rafRef = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
rafRef = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
if (rafRef !== null) {
|
||||
cancelAnimationFrame(rafRef);
|
||||
rafRef = null;
|
||||
}
|
||||
lastTimestampRef = null;
|
||||
};
|
||||
};
|
||||
|
||||
let cleanupResize: (() => void) | undefined;
|
||||
let cleanupImages: (() => void) | undefined;
|
||||
let cleanupAnimation: (() => void) | undefined;
|
||||
|
||||
const cleanup = () => {
|
||||
cleanupResize?.();
|
||||
cleanupImages?.();
|
||||
cleanupAnimation?.();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
cleanupResize = setupResizeObserver();
|
||||
cleanupImages = setupImageLoader();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => props.logos, () => props.gap, () => props.logoHeight],
|
||||
async () => {
|
||||
await nextTick();
|
||||
cleanupImages?.();
|
||||
cleanupImages = setupImageLoader();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h } from 'vue';
|
||||
|
||||
const LogoContent = defineComponent({
|
||||
name: 'LogoContent',
|
||||
props: {
|
||||
item: {
|
||||
type: Object as () => LogoItem,
|
||||
required: true
|
||||
},
|
||||
scaleOnHover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const isNodeItem = (item: LogoItem): item is LogoItemNode => 'node' in item;
|
||||
|
||||
return () => {
|
||||
const baseClasses = ['inline-flex items-center', 'motion-reduce:transition-none'];
|
||||
|
||||
if (props.scaleOnHover) {
|
||||
baseClasses.push(
|
||||
'transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover/item:scale-120'
|
||||
);
|
||||
}
|
||||
|
||||
if (isNodeItem(props.item)) {
|
||||
return h('span', {
|
||||
class: [
|
||||
...baseClasses,
|
||||
'text-[length:var(--logoloop-logoHeight)] [&>i]:text-[length:var(--logoloop-logoHeight)] [&>i]:leading-[1]'
|
||||
],
|
||||
innerHTML: props.item.node,
|
||||
'aria-hidden': !!(props.item as LogoItemNode).href && !(props.item as LogoItemNode).ariaLabel
|
||||
});
|
||||
} else {
|
||||
const imgClasses = [
|
||||
'h-[var(--logoloop-logoHeight)] w-auto block object-contain',
|
||||
'[-webkit-user-drag:none] pointer-events-none',
|
||||
'[image-rendering:-webkit-optimize-contrast]',
|
||||
'motion-reduce:transition-none'
|
||||
];
|
||||
|
||||
if (props.scaleOnHover) {
|
||||
imgClasses.push(
|
||||
'transition-transform duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover/item:scale-120'
|
||||
);
|
||||
}
|
||||
|
||||
return h('img', {
|
||||
class: imgClasses,
|
||||
src: props.item.src,
|
||||
srcset: props.item.srcSet,
|
||||
sizes: props.item.sizes,
|
||||
width: props.item.width,
|
||||
height: props.item.height,
|
||||
alt: props.item.alt ?? '',
|
||||
title: props.item.title,
|
||||
loading: 'lazy',
|
||||
decoding: 'async',
|
||||
draggable: false
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export { LogoContent };
|
||||
</script>
|
||||
174
src/demo/Animations/LogoLoopDemo.vue
Normal file
174
src/demo/Animations/LogoLoopDemo.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<TabbedLayout>
|
||||
<template #preview>
|
||||
<div class="relative h-[500px] overflow-hidden demo-container">
|
||||
<LogoLoop
|
||||
:key="key"
|
||||
:logos="items"
|
||||
width="100%"
|
||||
:gap="gap"
|
||||
:speed="speed"
|
||||
:direction="direction"
|
||||
:scale-on-hover="scaleOnHover"
|
||||
:pause-on-hover="pauseOnHover"
|
||||
:fade-out="fadeOut"
|
||||
fade-out-color="#060010"
|
||||
aria-label="Our icons"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Customize>
|
||||
<PreviewSlider title="Speed" :min="0" :max="300" :step="10" value-unit="px/s" v-model="speed" />
|
||||
<PreviewSlider title="Gap" :min="10" :max="120" :step="5" value-unit="px" v-model="gap" />
|
||||
<PreviewSelect title="Direction" v-model="direction" :options="directionOptions" />
|
||||
<PreviewSwitch title="Pause on Hover" v-model="pauseOnHover" />
|
||||
<PreviewSwitch title="Fade Out" v-model="fadeOut" />
|
||||
<PreviewSwitch title="Scale on Hover" v-model="scaleOnHover" />
|
||||
</Customize>
|
||||
|
||||
<PropTable :data="propData" />
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<CodeExample :code-object="logoLoop" />
|
||||
</template>
|
||||
|
||||
<template #cli>
|
||||
<CliInstallation :command="logoLoop.cli" />
|
||||
</template>
|
||||
</TabbedLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useForceRerender } from '@/composables/useForceRerender';
|
||||
import { logoLoop } from '@/constants/code/Animations/logoLoopCode';
|
||||
import { ref } from 'vue';
|
||||
import CliInstallation from '../../components/code/CliInstallation.vue';
|
||||
import CodeExample from '../../components/code/CodeExample.vue';
|
||||
import Customize from '../../components/common/Customize.vue';
|
||||
import PreviewSelect from '../../components/common/PreviewSelect.vue';
|
||||
import PreviewSlider from '../../components/common/PreviewSlider.vue';
|
||||
import PreviewSwitch from '../../components/common/PreviewSwitch.vue';
|
||||
import PropTable from '../../components/common/PropTable.vue';
|
||||
import TabbedLayout from '../../components/common/TabbedLayout.vue';
|
||||
import LogoLoop from '../../content/Animations/LogoLoop/LogoLoop.vue';
|
||||
|
||||
const { rerenderKey: key } = useForceRerender();
|
||||
|
||||
const speed = ref(100);
|
||||
const gap = ref(60);
|
||||
const pauseOnHover = ref(true);
|
||||
const fadeOut = ref(true);
|
||||
const scaleOnHover = ref(true);
|
||||
const direction = ref<'left' | 'right'>('left');
|
||||
|
||||
const directionOptions = [
|
||||
{ value: 'left', label: 'Left' },
|
||||
{ value: 'right', label: 'Right' }
|
||||
];
|
||||
|
||||
const items = [
|
||||
{ node: `<i class="pi pi-cog" style="font-size: 4rem;"></i>`, title: 'Settings', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-globe" style="font-size: 4rem;"></i>`, title: 'Web', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-code" style="font-size: 4rem;"></i>`, title: 'Code', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-palette" style="font-size: 4rem;"></i>`, title: 'Design', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-cloud" style="font-size: 4rem;"></i>`, title: 'Cloud', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-github" style="font-size: 4rem;"></i>`, title: 'GitHub', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-box" style="font-size: 4rem;"></i>`, title: 'Container', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-database" style="font-size: 4rem;"></i>`, title: 'Database', href: 'https://vue-bits.dev/' },
|
||||
{ node: `<i class="pi pi-server" style="font-size: 4rem;"></i>`, title: 'Server', href: 'https://vue-bits.dev/' },
|
||||
{
|
||||
node: `<i class="pi pi-credit-card" style="font-size: 4rem;"></i>`,
|
||||
title: 'Payments',
|
||||
href: 'https://vue-bits.dev/'
|
||||
}
|
||||
];
|
||||
|
||||
const propData = [
|
||||
{
|
||||
name: 'logos',
|
||||
type: 'LogoItem[]',
|
||||
default: 'required',
|
||||
description: 'Array of logo items to display. Each item can be either a React node or an image src.'
|
||||
},
|
||||
{
|
||||
name: 'speed',
|
||||
type: 'number',
|
||||
default: '120',
|
||||
description:
|
||||
'Animation speed in pixels per second. Positive values move based on direction, negative values reverse direction.'
|
||||
},
|
||||
{
|
||||
name: 'direction',
|
||||
type: "'left' | 'right'",
|
||||
default: "'left'",
|
||||
description: 'Direction of the logo animation loop.'
|
||||
},
|
||||
{
|
||||
name: 'width',
|
||||
type: 'number | string',
|
||||
default: "'100%'",
|
||||
description: 'Width of the logo loop container.'
|
||||
},
|
||||
{
|
||||
name: 'logoHeight',
|
||||
type: 'number',
|
||||
default: '28',
|
||||
description: 'Height of the logos in pixels.'
|
||||
},
|
||||
{
|
||||
name: 'gap',
|
||||
type: 'number',
|
||||
default: '32',
|
||||
description: 'Gap between logos in pixels.'
|
||||
},
|
||||
{
|
||||
name: 'pauseOnHover',
|
||||
type: 'boolean',
|
||||
default: 'true',
|
||||
description: 'Whether to pause the animation when hovering over the component.'
|
||||
},
|
||||
{
|
||||
name: 'fadeOut',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Whether to apply fade-out effect at the edges of the container.'
|
||||
},
|
||||
{
|
||||
name: 'fadeOutColor',
|
||||
type: 'string',
|
||||
default: 'undefined',
|
||||
description: 'Color used for the fade-out effect. Only applies when fadeOut is true.'
|
||||
},
|
||||
{
|
||||
name: 'scaleOnHover',
|
||||
type: 'boolean',
|
||||
default: 'false',
|
||||
description: 'Whether to scale logos on hover.'
|
||||
},
|
||||
{
|
||||
name: 'ariaLabel',
|
||||
type: 'string',
|
||||
default: "'Partner logos'",
|
||||
description: 'Accessibility label for the logo loop component.'
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
type: 'string',
|
||||
default: 'undefined',
|
||||
description: 'Additional CSS class names to apply to the root element.'
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
type: 'object',
|
||||
default: '{}',
|
||||
description: 'Inline styles to apply to the root element.'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.demo-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -188,7 +188,7 @@ const propData = [
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
type: 'React.CSSProperties',
|
||||
type: 'object',
|
||||
default: '{}',
|
||||
description: 'Inline styles.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user