feat(ui): add AppButton component

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-07-27 18:53:12 -04:00
parent 1e2a8c5b1e
commit ebf7435221
2 changed files with 66 additions and 0 deletions

View file

@ -0,0 +1,35 @@
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import AppButton from './AppButton.vue';
describe('AppButton', () => {
it('renders slot content', () => {
const wrapper = mount(AppButton, { slots: { default: 'Download' } });
expect(wrapper.text()).toBe('Download');
});
it('applies the primary accent style by default', () => {
const wrapper = mount(AppButton, { slots: { default: 'Go' } });
expect(wrapper.get('button').classes()).toContain('bg-accent');
});
it('applies the danger variant', () => {
const wrapper = mount(AppButton, {
props: { variant: 'danger' }, slots: { default: 'Delete' },
});
expect(wrapper.get('button').classes()).toContain('bg-danger');
});
it('emits click when enabled', async () => {
const wrapper = mount(AppButton, { slots: { default: 'Go' } });
await wrapper.get('button').trigger('click');
expect(wrapper.emitted('click')).toHaveLength(1);
});
it('reflects the disabled prop', () => {
const wrapper = mount(AppButton, {
props: { disabled: true }, slots: { default: 'Go' },
});
expect(wrapper.get('button').attributes('disabled')).toBeDefined();
});
});

View file

@ -0,0 +1,31 @@
<template>
<button class="rounded font-bold focus:outline-none focus-visible:ring-2
focus-visible:ring-accent transition-colors disabled:opacity-75
disabled:cursor-not-allowed"
:class="[variantClass, sizeClass]" :disabled="disabled">
<slot/>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = withDefaults(defineProps<{
variant?: 'primary' | 'secondary' | 'danger' | 'success';
size?: 'md' | 'sm';
disabled?: boolean;
}>(), {
variant: 'primary',
size: 'md',
disabled: false,
});
const variantClass = computed(() => ({
primary: 'bg-accent text-accent-fg',
secondary: 'bg-raised text-fg border border-edge',
danger: 'bg-danger text-white',
success: 'bg-success text-white',
}[props.variant]));
const sizeClass = computed(() => (props.size === 'sm' ? 'px-3 py-2 text-sm' : 'px-6 py-2.5'));
</script>