feat(stage-ui): added LevelMeter component

This commit is contained in:
Neko Ayaka
2025-07-02 03:17:34 +08:00
parent 2cb602aa3e
commit c00e11290a
3 changed files with 389 additions and 0 deletions
@@ -0,0 +1,305 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue'
import LevelMeter from './LevelMeter.vue'
// Reactive data for demos
const audioLevel = ref(45)
const batteryLevel = ref(78)
const cpuUsage = ref(23)
const animatedLevel = ref(0)
// Animation for demo
let animationFrame: number | null = null
const startTime = Date.now()
function animate() {
const elapsed = (Date.now() - startTime) / 1000
// Create a sine wave pattern
animatedLevel.value = 50 + Math.sin(elapsed * 2) * 30
// Update audio level with some randomness
audioLevel.value = Math.max(0, Math.min(100, audioLevel.value + (Math.random() - 0.5) * 10))
// Update CPU usage
cpuUsage.value = Math.max(0, Math.min(100, cpuUsage.value + (Math.random() - 0.5) * 5))
animationFrame = requestAnimationFrame(animate)
}
onMounted(() => {
animate()
})
onUnmounted(() => {
if (animationFrame) {
cancelAnimationFrame(animationFrame)
}
})
// Custom formatters
const formatDb = (value: number) => `${value.toFixed(1)} dB`
const formatTemp = (value: number) => `${Math.round(value)}°C`
const formatBattery = (value: number) => `${Math.round(value)}%`
// Custom color schemes
const temperatureThresholds = [
{ value: 40, color: 'bg-blue-500' },
{ value: 60, color: 'bg-green-500' },
{ value: 80, color: 'bg-yellow-500' },
{ value: 100, color: 'bg-red-500' },
]
const performanceThresholds = [
{ value: 50, color: 'bg-green-500' },
{ value: 75, color: 'bg-yellow-500' },
{ value: 90, color: 'bg-orange-500' },
{ value: 100, color: 'bg-red-500' },
]
</script>
<template>
<Story
title="Level Meter"
group="gadgets"
:layout="{ type: 'grid', width: '100%' }"
>
<template #controls>
<ThemeColorsHueControl />
</template>
<Variant
id="basic-examples"
title="Basic Examples"
>
<div class="p-2 space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2">
<!-- Audio Level -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Audio Level (Real-time)
</h3>
<LevelMeter
:level="audioLevel"
label="Input Level"
unit="%"
/>
</div>
<!-- Battery Level -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Battery Level
</h3>
<LevelMeter
:level="batteryLevel"
label="Battery"
unit="%"
:format-value="formatBattery"
/>
</div>
<!-- CPU Usage -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
CPU Usage (Animated)
</h3>
<LevelMeter
:level="cpuUsage"
label="CPU Usage"
unit="%"
:color-thresholds="performanceThresholds"
/>
</div>
</div>
</div>
</Variant>
<Variant
id="custom-styling"
title="Custom Styling"
>
<div class="p-2 space-y-6">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Temperature (Custom Colors) -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Temperature Monitor
</h3>
<LevelMeter
:level="65"
:min="0"
:max="100"
label="CPU Temperature"
unit="°C"
:height="32"
:color-thresholds="temperatureThresholds"
:format-value="formatTemp"
/>
</div>
<!-- Audio dB Level -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Audio Level (dB)
</h3>
<LevelMeter
:level="animatedLevel"
:min="-60"
:max="0"
label="Audio Level"
unit=" dB"
:height="28"
:format-value="formatDb"
:animation-speed="150"
/>
</div>
</div>
</div>
</Variant>
<Variant
id="configuration-options"
title="Configuration Options"
>
<div class="p-2 space-y-6">
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 md:grid-cols-2">
<!-- Compact (No Header) -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Compact Mode
</h3>
<LevelMeter
:level="72"
:show-header="false"
:height="20"
/>
<p class="mt-2 text-sm text-neutral-500">
No header, minimal height
</p>
</div>
<!-- Many Bars -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
High Resolution
</h3>
<LevelMeter
:level="88"
:num-bars="40"
label="High Precision"
:height="24"
/>
<p class="mt-2 text-sm text-neutral-500">
40 bars for fine detail
</p>
</div>
<!-- Few Bars -->
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Low Resolution
</h3>
<LevelMeter
:level="45"
:num-bars="8"
label="Coarse Display"
:height="32"
/>
<p class="mt-2 text-sm text-neutral-500">
8 bars for simplicity
</p>
</div>
</div>
</div>
</Variant>
<Variant
id="dashboard-example"
title="Dashboard Example"
>
<div class="p-2 space-y-6">
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-6 text-lg text-neutral-700 font-medium dark:text-neutral-300">
System Monitor Dashboard
</h3>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-4 md:grid-cols-2">
<div>
<LevelMeter
:level="67"
label="Memory Usage"
unit="%"
:color-thresholds="performanceThresholds"
:height="20"
/>
</div>
<div>
<LevelMeter
:level="34"
label="Disk Usage"
unit="%"
:height="20"
/>
</div>
<div>
<LevelMeter
:level="89"
label="Network"
unit="%"
:color-thresholds="performanceThresholds"
:height="20"
/>
</div>
<div>
<LevelMeter
:level="12"
label="GPU Usage"
unit="%"
:height="20"
/>
</div>
</div>
</div>
</div>
</Variant>
<Variant
id="interactive-demo"
title="Interactive Demo"
>
<div class="p-2 space-y-6">
<div class="rounded-xl bg-white p-6 shadow-md dark:bg-neutral-800">
<h3 class="mb-4 text-lg text-neutral-700 font-medium dark:text-neutral-300">
Adjustable Level
</h3>
<div class="space-y-4">
<LevelMeter
:level="batteryLevel"
label="Adjustable Demo"
unit="%"
:height="32"
/>
<div>
<label class="mb-2 block text-sm text-neutral-600 font-medium dark:text-neutral-400">
Adjust Level: {{ batteryLevel }}%
</label>
<input
v-model.number="batteryLevel"
type="range"
min="0"
max="100"
class="h-2 w-full cursor-pointer appearance-none rounded-lg bg-neutral-200 dark:bg-neutral-700"
>
</div>
</div>
</div>
</div>
</Variant>
</Story>
</template>
@@ -0,0 +1,83 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
level: number // Current level value
min?: number // Minimum value
max?: number // Maximum value
numBars?: number // Number of bars
label?: string // Display label
unit?: string // Unit suffix (%, dB, etc.)
height?: number // Height in pixels
showHeader?: boolean // Show label and value
animationSpeed?: number // Animation duration in ms
colorThresholds?: { value: number, color: string }[] // Custom color thresholds
formatValue?: (value: number) => string // Custom value formatter
}
const props = withDefaults(defineProps<Props>(), {
min: 0,
max: 100,
numBars: 20,
label: 'Level',
unit: '%',
height: 24,
showHeader: true,
animationSpeed: 75,
colorThresholds: () => [
{ value: 60, color: 'bg-green-500' },
{ value: 80, color: 'bg-yellow-500' },
{ value: 100, color: 'bg-red-500' },
],
})
const levelBars = computed(() => {
const normalizedLevel = Math.max(0, Math.min(100, ((props.level - props.min) / (props.max - props.min)) * 100))
const activeBars = Math.floor((normalizedLevel / 100) * props.numBars)
return Array.from({ length: props.numBars }, (_, i) => ({
active: i < activeBars,
level: (i / props.numBars) * 100, // Percentage of this bar position
}))
})
function getBarColor(index: number, barLevel: number): string {
const thresholds = [...props.colorThresholds].sort((a, b) => a.value - b.value)
for (const threshold of thresholds) {
if (barLevel <= threshold.value) {
return threshold.color
}
}
return thresholds[thresholds.length - 1]?.color || 'bg-green-500'
}
</script>
<template>
<div>
<div v-if="showHeader" class="mb-2 flex items-center justify-between">
<span class="text-sm font-medium">{{ label }}</span>
<span class="text-sm text-neutral-500">
{{ formatValue ? formatValue(level) : `${Math.round(level)}${unit}` }}
</span>
</div>
<!-- Level Bars -->
<div
class="flex items-end gap-1 rounded bg-neutral-200 p-1 dark:bg-neutral-700"
:style="{ height: `${height}px` }"
>
<div
v-for="(bar, index) in levelBars"
:key="index"
class="flex-1 rounded-sm transition-all"
:class="[
bar.active ? getBarColor(index, bar.level) : 'bg-neutral-300 dark:bg-neutral-600',
`duration-${animationSpeed}`,
]"
:style="{ height: bar.active ? '100%' : '20%' }"
/>
</div>
</div>
</template>
@@ -1,3 +1,4 @@
export { default as AudioSpectrum } from './AudioSpectrum.vue'
export { default as AudioSpectrumVisualizer } from './AudioSpectrumVisualizer.vue'
export { default as LevelMeter } from './LevelMeter.vue'
export { default as TestDummyMarker } from './TestDummyMarker.vue'