Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
<template>
<div id="app">
<router-view />
<div id="app" class="transition-colors duration-200">
<PageTransition>
<router-view />
</PageTransition>
</div>
</template>

<script setup lang="ts">
// App root component
import { onMounted } from 'vue'
import PageTransition from '@/components/PageTransition.vue'
import { useTheme } from '@/composables/useTheme'

// Initialize theme on app start
const { initTheme } = useTheme()

onMounted(() => {
initTheme()
})
</script>

<style>
Expand Down
106 changes: 106 additions & 0 deletions src/components/BaseBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<template>
<span :class="badgeClasses" v-bind="$attrs">
<span v-if="$slots.icon || dot" class="flex items-center mr-1" :class="{ 'mr-0': !$slots.default }" aria-hidden="true">
<slot name="icon">
<div v-if="dot" :class="dotClasses"></div>
</slot>
</span>

<span v-if="$slots.default">
<slot />
</span>

<span v-if="$slots.iconRight" class="flex items-center ml-1" aria-hidden="true">
<slot name="iconRight" />
</span>
</span>
</template>

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
variant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info'
size?: 'xs' | 'sm' | 'md' | 'lg'
dot?: boolean
rounded?: boolean
outline?: boolean
pulse?: boolean
}

const props = withDefaults(defineProps<Props>(), {
variant: 'default',
size: 'md',
dot: false,
rounded: false,
outline: false,
pulse: false
})

// Base classes
const baseClasses = 'inline-flex items-center font-medium transition-all duration-200'

// Size classes
const sizeClasses = {
xs: 'px-2 py-0.5 text-xs',
sm: 'px-2.5 py-1 text-xs',
md: 'px-3 py-1 text-sm',
lg: 'px-4 py-1.5 text-sm'
}

// Variant classes (filled)
const variantClasses = {
default: 'bg-gray-100 text-gray-700',
primary: 'bg-blue-100 text-blue-700',
secondary: 'bg-gray-100 text-gray-700',
success: 'bg-green-100 text-green-700',
warning: 'bg-yellow-100 text-yellow-700',
danger: 'bg-red-100 text-red-700',
info: 'bg-cyan-100 text-cyan-700'
}

// Outline variant classes
const outlineVariantClasses = {
default: 'border border-gray-300 text-gray-700 bg-white',
primary: 'border border-blue-300 text-blue-700 bg-white',
secondary: 'border border-gray-300 text-gray-700 bg-white',
success: 'border border-green-300 text-green-700 bg-white',
warning: 'border border-yellow-300 text-yellow-700 bg-white',
danger: 'border border-red-300 text-red-700 bg-white',
info: 'border border-cyan-300 text-cyan-700 bg-white'
}

// Dot color classes
const dotColorClasses = {
default: 'bg-gray-400',
primary: 'bg-blue-500',
secondary: 'bg-gray-400',
success: 'bg-green-500',
warning: 'bg-yellow-500',
danger: 'bg-red-500',
info: 'bg-cyan-500'
}

// Computed classes
const badgeClasses = computed(() => [
baseClasses,
sizeClasses[props.size],
props.outline ? outlineVariantClasses[props.variant] : variantClasses[props.variant],
{
'rounded-full': props.rounded || props.size === 'xs',
'rounded-lg': !props.rounded && props.size !== 'xs',
'animate-pulse': props.pulse
}
])

const dotClasses = computed(() => [
'rounded-full',
dotColorClasses[props.variant],
{
'w-1.5 h-1.5': props.size === 'xs',
'w-2 h-2': props.size === 'sm' || props.size === 'md',
'w-2.5 h-2.5': props.size === 'lg',
'animate-pulse': props.pulse
}
])
</script>
117 changes: 117 additions & 0 deletions src/components/BaseButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<template>
<component
:is="tag"
:to="to"
:href="href"
:target="target"
:rel="rel"
:type="type"
:disabled="disabled || loading"
:class="buttonClasses"
:aria-label="ariaLabel"
:aria-describedby="ariaDescribedby"
@click="handleClick"
v-bind="$attrs"
>
<span v-if="loading" class="flex items-center justify-center mr-2" aria-hidden="true">
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="sr-only">Loading...</span>
</span>

<span v-if="$slots.icon && !loading" class="flex items-center mr-2" aria-hidden="true">
<slot name="icon" />
</span>

<span :class="{ 'opacity-75': loading }">
<slot />
</span>

<span v-if="$slots.iconRight && !loading" class="flex items-center ml-2" aria-hidden="true">
<slot name="iconRight" />
</span>
</component>
</template>

<script setup lang="ts">
import { computed } from 'vue'

interface Props {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
tag?: 'button' | 'a' | 'router-link'
to?: string | object
href?: string
target?: string
rel?: string
type?: 'button' | 'submit' | 'reset'
disabled?: boolean
loading?: boolean
block?: boolean
rounded?: boolean
shadow?: boolean
ariaLabel?: string
ariaDescribedby?: string
}

const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
tag: 'button',
type: 'button',
disabled: false,
loading: false,
block: false,
rounded: false,
shadow: true
})

const emit = defineEmits<{
click: [event: Event]
}>()

// Base classes
const baseClasses = 'inline-flex items-center justify-center font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50'

// Size classes
const sizeClasses = {
xs: 'px-2.5 py-1.5 text-xs',
sm: 'px-3 py-2 text-sm',
md: 'px-4 py-2.5 text-sm',
lg: 'px-6 py-3 text-base',
xl: 'px-8 py-4 text-lg'
}

// Variant classes
const variantClasses = {
primary: 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white focus:ring-blue-500',
secondary: 'bg-gray-100 hover:bg-gray-200 text-gray-900 focus:ring-gray-500',
outline: 'border-2 border-gray-300 hover:border-blue-600 bg-white hover:bg-blue-50 text-gray-700 hover:text-blue-600 focus:ring-blue-500',
ghost: 'bg-transparent hover:bg-gray-100 text-gray-700 hover:text-gray-900 focus:ring-gray-500',
danger: 'bg-red-600 hover:bg-red-700 text-white focus:ring-red-500'
}

// Computed classes
const buttonClasses = computed(() => [
baseClasses,
sizeClasses[props.size],
variantClasses[props.variant],
{
'w-full': props.block,
'rounded-xl': props.rounded || props.size === 'lg' || props.size === 'xl',
'rounded-lg': !props.rounded && (props.size === 'md' || props.size === 'sm'),
'rounded-md': !props.rounded && props.size === 'xs',
'shadow-lg hover:shadow-xl transform hover:-translate-y-0.5': props.shadow && props.variant === 'primary',
'shadow-md hover:shadow-lg': props.shadow && props.variant !== 'primary',
'cursor-wait': props.loading
}
])

const handleClick = (event: Event) => {
if (!props.disabled && !props.loading) {
emit('click', event)
}
}
</script>
Loading