feat(gui-v2): EnergyPicker interactive 5-step (crewli-starter port) + story
This commit is contained in:
18
apps/app/src/components-v2/shared/EnergyPicker.stories.ts
Normal file
18
apps/app/src/components-v2/shared/EnergyPicker.stories.ts
Normal 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>',
|
||||||
|
}),
|
||||||
|
}
|
||||||
42
apps/app/src/components-v2/shared/EnergyPicker.vue
Normal file
42
apps/app/src/components-v2/shared/EnergyPicker.vue
Normal 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>
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user