feat(gui-v2): EnergyDots 5-dot meter (scoped CSS justified per §8) + story

This commit is contained in:
2026-05-18 11:16:57 +02:00
parent b64b024166
commit 79650d0b72
3 changed files with 74 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import EnergyDots from '@/components-v2/shared/EnergyDots.vue'
const meta: Meta<typeof EnergyDots> = {
title: 'Shared/EnergyDots',
component: EnergyDots,
tags: ['autodocs'],
argTypes: { value: { control: { type: 'range', min: 0, max: 5, step: 1 } }, lg: { control: 'boolean' } },
}
export default meta
type Story = StoryObj<typeof EnergyDots>
export const Level3: Story = { args: { value: 3 } }
export const Level5: Story = { args: { value: 5 } }
export const Large: Story = { args: { value: 4, lg: true } }

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
/**
* EnergyDots — 5-dot meter (spec §8: no PrimeVue primitive; Rating is
* stars/wrong visual → minimal scoped CSS is the justified bespoke case).
* crewli-starter main.css 19821991 ported; crewli vars → Aura tokens.
*/
import { computed } from 'vue'
const props = withDefaults(defineProps<{ value?: number; lg?: boolean }>(), { value: 0, lg: false })
const clamped = computed(() => Math.max(0, Math.min(5, Math.round(props.value))))
</script>
<template>
<div
class="energy-dots"
:class="{ lg }"
:data-energy="clamped"
>
<span
v-for="i in 5"
:key="i"
class="d"
:class="{ on: i <= clamped }"
/>
</div>
</template>
<style scoped>
/* Justified per spec §8 — no Tailwind/PrimeVue expression of this meter. */
.energy-dots { display: inline-flex; gap: 3px; align-items: center; }
.d { width: 8px; height: 8px; border-radius: 50%; background: var(--p-content-border-color); }
.d.on { background: var(--p-primary-color); }
.energy-dots[data-energy="1"] .d.on { background: var(--p-sky-600); }
.energy-dots[data-energy="4"] .d.on { background: oklch(65% 0.15 35); }
.energy-dots[data-energy="5"] .d.on { background: var(--p-red-600); }
.energy-dots.lg .d { width: 11px; height: 11px; }
</style>

View File

@@ -0,0 +1,22 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import EnergyDots from '@/components-v2/shared/EnergyDots.vue'
describe('EnergyDots', () => {
it('always renders 5 dots; `on` count equals value', () => {
const w = mount(EnergyDots, { props: { value: 3 } })
expect(w.findAll('.d')).toHaveLength(5)
expect(w.findAll('.d.on')).toHaveLength(3)
})
it('exposes data-energy for level colouring and lg class when lg', () => {
const w = mount(EnergyDots, { props: { value: 5, lg: true } })
expect(w.get('[data-energy]').attributes('data-energy')).toBe('5')
expect(w.get('.energy-dots').classes()).toContain('lg')
})
it('clamps value into 0..5', () => {
expect(mount(EnergyDots, { props: { value: 9 } }).findAll('.d.on')).toHaveLength(5)
expect(mount(EnergyDots, { props: { value: -2 } }).findAll('.d.on')).toHaveLength(0)
})
})