mirror of
https://github.com/DavidHDev/vue-bits.git
synced 2026-03-07 22:49:31 -07:00
Create <Counter /> component
This commit is contained in:
@@ -80,7 +80,8 @@ export const CATEGORIES = [
|
|||||||
'Flowing Menu',
|
'Flowing Menu',
|
||||||
'Elastic Slider',
|
'Elastic Slider',
|
||||||
'Stack',
|
'Stack',
|
||||||
'Chroma Grid'
|
'Chroma Grid',
|
||||||
|
'Counter'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgrounds = {
|
const backgrounds = {
|
||||||
|
|||||||
17
src/constants/code/Components/counterCode.ts
Normal file
17
src/constants/code/Components/counterCode.ts
Normal 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"
|
||||||
|
/>`,
|
||||||
|
})
|
||||||
219
src/content/Components/Counter/Counter.vue
Normal file
219
src/content/Components/Counter/Counter.vue
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<template>
|
||||||
|
<div class="counter-container" :style="containerStyle">
|
||||||
|
<div
|
||||||
|
class="counter-counter"
|
||||||
|
:style="counterStyles"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="place in places"
|
||||||
|
:key="place"
|
||||||
|
class="counter-digit"
|
||||||
|
:style="digitStyles"
|
||||||
|
>
|
||||||
|
<Motion
|
||||||
|
v-for="digit in 10"
|
||||||
|
:key="digit - 1"
|
||||||
|
tag="span"
|
||||||
|
class="counter-number"
|
||||||
|
:animate="{ y: getDigitPosition(place, digit - 1) }"
|
||||||
|
>
|
||||||
|
{{ digit - 1 }}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gradient-container">
|
||||||
|
<div class="top-gradient" :style="topGradientStyle || topGradientStyles" />
|
||||||
|
<div class="bottom-gradient" :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>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.counter-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-counter {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-digit {
|
||||||
|
position: relative;
|
||||||
|
width: 1ch;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-number {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
will-change: transform;
|
||||||
|
transform: translateZ(0);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-container {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-gradient {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-gradient {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
195
src/demo/Components/CounterDemo.vue
Normal file
195
src/demo/Components/CounterDemo.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user