feat(gui-v2): EnergyPicker interactive 5-step (crewli-starter port) + story

This commit is contained in:
2026-05-18 11:25:27 +02:00
parent 79650d0b72
commit 91d20d0dd2
3 changed files with 85 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import EnergyPicker from '@/components-v2/shared/EnergyPicker.vue'
const meta: Meta<typeof EnergyPicker> = { title: 'Shared/EnergyPicker', component: EnergyPicker, tags: ['autodocs'] }
export default meta
type Story = StoryObj<typeof EnergyPicker>
export const Interactive: Story = {
render: () => ({
components: { EnergyPicker },
setup() {
const v = ref(0)
return { v }
},
template: '<div class="flex items-center gap-3"><EnergyPicker v-model="v" /><span class="text-sm">value: {{ v }}</span></div>',
}),
}

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
/**
* EnergyPicker — interactive 5-step picker (crewli-starter music/
* EnergyPicker.vue port). Click current value → reset to 0. Scoped CSS
* justified per spec §8 (same rationale as EnergyDots).
*/
const props = withDefaults(defineProps<{ modelValue?: number }>(), { modelValue: 0 })
const emit = defineEmits<{ 'update:modelValue': [number] }>()
function pick(i: number): void {
emit('update:modelValue', i === props.modelValue ? 0 : i)
}
</script>
<template>
<div class="energy-picker">
<button
v-for="i in 5"
:key="i"
type="button"
:class="{ on: i <= modelValue }"
@click="pick(i)"
>
{{ i }}
</button>
</div>
</template>
<style scoped>
.energy-picker { display: inline-flex; gap: 4px; }
.energy-picker button {
width: 28px; height: 28px; border-radius: 50%;
border: 1px solid var(--p-content-border-color);
background: var(--p-content-background); color: var(--p-text-muted-color);
font-size: 12px; font-weight: 600; cursor: pointer;
transition: background .15s, color .15s, border-color .15s;
}
.energy-picker button.on {
background: var(--p-primary-color); color: var(--p-primary-contrast-color);
border-color: var(--p-primary-color);
}
</style>

View File

@@ -0,0 +1,25 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import EnergyPicker from '@/components-v2/shared/EnergyPicker.vue'
describe('EnergyPicker', () => {
it('renders 5 buttons; clicking i emits i', async () => {
const w = mount(EnergyPicker, { props: { modelValue: 0 } })
const btns = w.findAll('button')
expect(btns).toHaveLength(5)
await btns[2].trigger('click')
expect(w.emitted('update:modelValue')![0]).toEqual([3])
})
it('clicking the current value toggles back to 0 (crewli-starter parity)', async () => {
const w = mount(EnergyPicker, { props: { modelValue: 3 } })
await w.findAll('button')[2].trigger('click')
expect(w.emitted('update:modelValue')![0]).toEqual([0])
})
it('marks buttons up to modelValue as on', () => {
const w = mount(EnergyPicker, { props: { modelValue: 2 } })
expect(w.findAll('button.on')).toHaveLength(2)
})
})