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