feat(gui-v2): EnergyDots 5-dot meter (scoped CSS justified per §8) + story
This commit is contained in:
15
apps/app/src/components-v2/shared/EnergyDots.stories.ts
Normal file
15
apps/app/src/components-v2/shared/EnergyDots.stories.ts
Normal 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 } }
|
||||
37
apps/app/src/components-v2/shared/EnergyDots.vue
Normal file
37
apps/app/src/components-v2/shared/EnergyDots.vue
Normal 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 1982–1991 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>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user