Merge pull request #57 from Gazoon007/feat/counter

Create <Counter /> component
This commit is contained in:
David
2025-07-24 15:43:08 +03:00
committed by GitHub
5 changed files with 375 additions and 0 deletions

View File

@@ -82,6 +82,7 @@ export const CATEGORIES = [
'Elastic Slider', 'Elastic Slider',
'Stack', 'Stack',
'Chroma Grid', 'Chroma Grid',
'Counter',
'Rolling Gallery' 'Rolling Gallery'
] ]
}, },

View File

@@ -69,6 +69,7 @@ const components = {
'tilted-card': () => import('../demo/Components/TiltedCardDemo.vue'), 'tilted-card': () => import('../demo/Components/TiltedCardDemo.vue'),
'stack': () => import('../demo/Components/StackDemo.vue'), 'stack': () => import('../demo/Components/StackDemo.vue'),
'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'), 'chroma-grid': () => import('../demo/Components/ChromaGridDemo.vue'),
'counter': () => import('../demo/Components/CounterDemo.vue'),
'rolling-gallery': () => import('../demo/Components/RollingGalleryDemo.vue'), 'rolling-gallery': () => import('../demo/Components/RollingGalleryDemo.vue'),
'scroll-stack': () => import('../demo/Components/ScrollStackDemo.vue'), 'scroll-stack': () => import('../demo/Components/ScrollStackDemo.vue'),
}; };

View File

@@ -0,0 +1,17 @@
import code from '@content/Components/Counter/Counter.vue?raw'
import { createCodeObject } from '../../../types/code'
export const counter = createCodeObject(code, 'Components/Counter', {
installation: `npm i motion-v`,
usage: `import Counter from './Counter.vue'
<Counter
:value="1"
:places="[100, 10, 1]"
:fontSize="80"
:padding="5"
:gap="10"
textColor="white"
:fontWeight="900"
/>`,
})

View File

@@ -0,0 +1,161 @@
<template>
<div class="relative inline-block" :style="containerStyle">
<div class="flex overflow-hidden" :style="counterStyles">
<div v-for="place in places" :key="place" class="relative w-[1ch] tabular-nums" :style="digitStyles">
<Motion
v-for="digit in 10"
:key="digit - 1"
tag="span"
class="absolute top-0 left-0 w-full h-full flex items-center justify-center"
:animate="{ y: getDigitPosition(place, digit - 1) }"
>
{{ digit - 1 }}
</Motion>
</div>
</div>
<div class="pointer-events-none absolute inset-0">
<div class="absolute top-0 w-full" :style="topGradientStyle ?? topGradientStyles" />
<div class="absolute bottom-0 w-full" :style="bottomGradientStyle ?? bottomGradientStyles" />
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { Motion } from 'motion-v';
import type { CSSProperties } from 'vue';
interface CounterProps {
value: number;
fontSize?: number;
padding?: number;
places?: number[];
gap?: number;
borderRadius?: number;
horizontalPadding?: number;
textColor?: string;
fontWeight?: string | number;
containerStyle?: CSSProperties;
counterStyle?: CSSProperties;
digitStyle?: CSSProperties;
gradientHeight?: number;
gradientFrom?: string;
gradientTo?: string;
topGradientStyle?: CSSProperties;
bottomGradientStyle?: CSSProperties;
}
const props = withDefaults(defineProps<CounterProps>(), {
fontSize: 100,
padding: 0,
places: () => [100, 10, 1],
gap: 8,
borderRadius: 4,
horizontalPadding: 8,
textColor: 'white',
fontWeight: 'bold',
containerStyle: () => ({}),
counterStyle: () => ({}),
digitStyle: () => ({}),
gradientHeight: 16,
gradientFrom: 'black',
gradientTo: 'transparent',
topGradientStyle: undefined,
bottomGradientStyle: undefined
});
const digitHeight = computed(() => props.fontSize + props.padding);
const counterStyles = computed(() => ({
fontSize: `${props.fontSize}px`,
gap: `${props.gap}px`,
borderRadius: `${props.borderRadius}px`,
paddingLeft: `${props.horizontalPadding}px`,
paddingRight: `${props.horizontalPadding}px`,
color: props.textColor,
fontWeight: props.fontWeight,
...props.counterStyle
}));
const digitStyles = computed(() => ({
height: `${digitHeight.value}px`,
...props.digitStyle
}));
const topGradientStyles = computed(
(): CSSProperties => ({
height: `${props.gradientHeight}px`,
background: `linear-gradient(to bottom, ${props.gradientFrom}, ${props.gradientTo})`
})
);
const bottomGradientStyles = computed(
(): CSSProperties => ({
height: `${props.gradientHeight}px`,
background: `linear-gradient(to top, ${props.gradientFrom}, ${props.gradientTo})`
})
);
const springValues = ref<Record<number, number>>({});
const initializeSpringValues = () => {
props.places.forEach(place => {
springValues.value[place] = Math.floor(props.value / place);
});
};
initializeSpringValues();
watch(
() => props.value,
(newValue, oldValue) => {
if (newValue === oldValue) return;
props.places.forEach(place => {
const newRoundedValue = Math.floor(newValue / place);
const oldRoundedValue = springValues.value[place];
if (newRoundedValue !== oldRoundedValue) {
springValues.value[place] = newRoundedValue;
}
});
},
{ immediate: true }
);
watch(
() => digitHeight.value,
() => {
positionCache.clear();
}
);
const positionCache = new Map<string, number>();
const getDigitPosition = (place: number, digit: number): number => {
const springValue = springValues.value[place] || 0;
const cacheKey = `${place}-${digit}-${springValue}`;
if (positionCache.has(cacheKey)) {
return positionCache.get(cacheKey)!;
}
const placeValue = springValue % 10;
const offset = (10 + digit - placeValue) % 10;
let position = offset * digitHeight.value;
if (offset > 5) {
position -= 10 * digitHeight.value;
}
if (positionCache.size > 200) {
const firstKey = positionCache.keys().next().value;
if (typeof firstKey === 'string') {
positionCache.delete(firstKey);
}
}
positionCache.set(cacheKey, position);
return position;
};
</script>

View File

@@ -0,0 +1,195 @@
<template>
<TabbedLayout>
<template #preview>
<div class="demo-container h-[400px] overflow-hidden relative">
<Counter
:value="value"
:places="[100, 10, 1]"
gradientFrom="#060010"
:fontSize="fontSize"
:padding="5"
:gap="gap"
:borderRadius="10"
:horizontalPadding="15"
textColor="white"
:fontWeight="900"
/>
<div class="flex gap-4 bottom-4 justify-center mt-4 absolute left-1/2 transform -translate-x-1/2">
<button
class="bg-[#170D27] rounded-[10px] border border-[#271E37] hover:bg-[#271E37] text-white h-10 w-10 transition-colors"
@click="value > 0 && value--"
>
-
</button>
<button
class="bg-[#170D27] rounded-[10px] border border-[#271E37] hover:bg-[#271E37] text-white h-10 w-10 transition-colors"
@click="value < 999 && value++"
>
+
</button>
</div>
</div>
<Customize>
<PreviewSlider
title="Value"
v-model="value"
:min="0"
:max="999"
:step="1"
/>
<PreviewSlider
title="Gap"
v-model="gap"
:min="0"
:max="50"
:step="5"
/>
<PreviewSlider
title="Font Size"
v-model="fontSize"
:min="40"
:max="200"
:step="10"
/>
</Customize>
<PropTable :data="propData" />
<Dependencies :dependency-list="['motion-v']" />
</template>
<template #code>
<CodeExample :code-object="counter" />
</template>
<template #cli>
<CliInstallation :command="counter.cli" />
</template>
</TabbedLayout>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import TabbedLayout from '../../components/common/TabbedLayout.vue'
import Customize from '../../components/common/Customize.vue'
import PreviewSlider from '../../components/common/PreviewSlider.vue'
import PropTable from '../../components/common/PropTable.vue'
import CodeExample from '../../components/code/CodeExample.vue'
import CliInstallation from '../../components/code/CliInstallation.vue'
import Dependencies from '../../components/code/Dependencies.vue'
import Counter from '../../content/Components/Counter/Counter.vue'
import { counter } from '../../constants/code/Components/counterCode'
const value = ref(123)
const fontSize = ref(80)
const gap = ref(10)
const propData = [
{
name: "value",
type: "number",
default: "N/A (required)",
description: "The numeric value to display in the counter."
},
{
name: "fontSize",
type: "number",
default: "100",
description: "The base font size used for the counter digits."
},
{
name: "padding",
type: "number",
default: "0",
description: "Additional padding added to the digit height."
},
{
name: "places",
type: "number[]",
default: "[100, 10, 1]",
description: "An array of place values to determine which digits to display."
},
{
name: "gap",
type: "number",
default: "8",
description: "The gap (in pixels) between each digit."
},
{
name: "borderRadius",
type: "number",
default: "4",
description: "The border radius (in pixels) for the counter container."
},
{
name: "horizontalPadding",
type: "number",
default: "8",
description: "The horizontal padding (in pixels) for the counter container."
},
{
name: "textColor",
type: "string",
default: "'white'",
description: "The text color for the counter digits."
},
{
name: "fontWeight",
type: "string | number",
default: "'bold'",
description: "The font weight of the counter digits."
},
{
name: "containerStyle",
type: "CSSProperties",
default: "{}",
description: "Custom inline styles for the outer container."
},
{
name: "counterStyle",
type: "CSSProperties",
default: "{}",
description: "Custom inline styles for the counter element."
},
{
name: "digitStyle",
type: "CSSProperties",
default: "{}",
description: "Custom inline styles for each digit container."
},
{
name: "gradientHeight",
type: "number",
default: "16",
description: "The height (in pixels) of the gradient overlays."
},
{
name: "gradientFrom",
type: "string",
default: "'black'",
description: "The starting color for the gradient overlays."
},
{
name: "gradientTo",
type: "string",
default: "'transparent'",
description: "The ending color for the gradient overlays."
},
{
name: "topGradientStyle",
type: "CSSProperties",
default: "undefined",
description: "Custom inline styles for the top gradient overlay."
},
{
name: "bottomGradientStyle",
type: "CSSProperties",
default: "undefined",
description: "Custom inline styles for the bottom gradient overlay."
},
]
</script>