feat(stage-ui): added LevelMeter component
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user