Component Library Overview
God Panel provides a comprehensive component library built with Vue 3 and TypeScript, offering reusable UI components for building modern admin interfaces.
Component Categories
🎨 UI Components
- Buttons: Primary, secondary, ghost, and icon buttons
- Cards: Content containers with headers, bodies, and footers
- Forms: Input fields, textareas, selects, checkboxes, and radios
- Modals: Dialog boxes for confirmations and content display
- Notifications: Toast messages and alerts
📊 Data Display
- Tables: Sortable, filterable data tables with pagination
- Charts: Integration with Chart.js for data visualization
- Stats: Metric cards and KPI displays
- Lists: Ordered and unordered lists with icons
🧭 Navigation
- Sidebar: Collapsible navigation with icons
- Breadcrumbs: Navigation trail components
- Tabs: Tabbed interface components
- Pagination: Page navigation controls
🔧 Form Controls
- Date Picker: Calendar-based date selection
- File Upload: Drag-and-drop file upload
- Rich Text Editor: WYSIWYG content editor
- Color Picker: Color selection tool
Component Architecture
Base Component Structure
vue
<!-- components/ui/BaseComponent.vue -->
<template>
<component
:is="tag"
:class="componentClasses"
v-bind="$attrs"
>
<slot />
</component>
</template>
<script setup>
interface Props {
tag?: string
variant?: string
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
tag: 'div',
variant: 'default',
size: 'md',
disabled: false,
loading: false
})
const componentClasses = computed(() => [
'base-component',
`base-component-${props.variant}`,
`base-component-${props.size}`,
{
'base-component-disabled': props.disabled,
'base-component-loading': props.loading
}
])
</script>Theme Integration
All components are built with CSS custom properties for seamless theme integration:
css
/* Component theming */
.btn-primary {
background-color: var(--color-primary);
color: var(--text-inverse);
border-color: var(--color-primary);
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-4);
font-weight: var(--font-medium);
transition: all 0.2s ease;
}
[data-theme="dark"] .btn-primary {
background-color: var(--color-primary-600);
border-color: var(--color-primary-600);
}Component Usage
Import and Registration
typescript
// Auto-import (recommended)
<script setup>
// Components are auto-imported via Nuxt
const showModal = ref(false)
</script>
<!-- Import specific components -->
<script setup>
import { Button, Card, Input } from '#components'
</script>Global Registration
typescript
// plugins/components.client.ts
import { defineNuxtPlugin } from '#app'
export default defineNuxtPlugin(() => {
// Components are auto-registered via Nuxt
})Component Props and Events
Standard Props
Most components accept these standard props:
typescript
interface BaseProps {
// Size variants
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
// Visual variants
variant?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'
// State
disabled?: boolean
loading?: boolean
readonly?: boolean
// Styling
class?: string
style?: Record<string, string>
}Event Handling
vue
<template>
<Button
@click="handleClick"
@focus="handleFocus"
@blur="handleBlur"
>
Click me
</Button>
</template>
<script setup>
const handleClick = (event: MouseEvent) => {
console.log('Button clicked', event)
}
const handleFocus = () => {
console.log('Button focused')
}
</script>Responsive Design
All components are built mobile-first and responsive:
css
/* Mobile-first approach */
.component {
width: 100%;
padding: var(--space-2);
}
/* Tablet */
@media (min-width: 768px) {
.component {
width: auto;
padding: var(--space-3);
}
}
/* Desktop */
@media (min-width: 1024px) {
.component {
padding: var(--space-4);
}
}Accessibility
Components follow WCAG 2.1 guidelines:
Keyboard Navigation
vue
<template>
<Button
ref="buttonRef"
@keydown.enter="handleEnter"
@keydown.space="handleSpace"
>
Accessible Button
</Button>
</template>
<script setup>
const buttonRef = ref()
const handleEnter = (event: KeyboardEvent) => {
event.preventDefault()
// Handle enter key
}
const handleSpace = (event: KeyboardEvent) => {
event.preventDefault()
// Handle space key
}
</script>Screen Reader Support
vue
<template>
<div role="button" :aria-label="label" tabindex="0">
<slot />
</div>
</template>
<script setup>
interface Props {
label: string
disabled?: boolean
}
const props = defineProps<Props>()
// Announce state changes
const announceToScreenReader = (message: string) => {
const announcement = document.createElement('div')
announcement.setAttribute('aria-live', 'polite')
announcement.textContent = message
document.body.appendChild(announcement)
setTimeout(() => {
document.body.removeChild(announcement)
}, 1000)
}
</script>Component Testing
Unit Tests
typescript
// test/components/Button.test.ts
import { mount } from '@vue/test-utils'
import Button from '~/components/ui/Button.vue'
describe('Button Component', () => {
test('renders correctly', () => {
const wrapper = mount(Button, {
props: {
variant: 'primary'
},
slots: {
default: 'Click me'
}
})
expect(wrapper.text()).toBe('Click me')
expect(wrapper.classes()).toContain('btn-primary')
})
test('emits click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
test('is disabled when disabled prop is true', () => {
const wrapper = mount(Button, {
props: {
disabled: true
}
})
expect(wrapper.attributes('disabled')).toBeDefined()
expect(wrapper.classes()).toContain('btn-disabled')
})
})Visual Regression Tests
typescript
// test/components/Button.visual.test.ts
describe('Button Visual Tests', () => {
test('matches snapshot for primary variant', async () => {
const wrapper = mount(Button, {
props: {
variant: 'primary'
}
})
expect(wrapper.html()).toMatchSnapshot()
})
test('matches snapshot for different sizes', async () => {
const sizes = ['xs', 'sm', 'md', 'lg', 'xl']
for (const size of sizes) {
const wrapper = mount(Button, {
props: { size }
})
expect(wrapper.html()).toMatchSnapshot()
}
})
})Performance Considerations
Lazy Loading
vue
<!-- Only load when needed -->
<template>
<component
:is="currentComponent"
v-if="showComponent"
/>
</template>
<script setup>
const showComponent = ref(false)
const currentComponent = ref('')
const loadComponent = async (componentName: string) => {
showComponent.value = false
// Dynamic import
const component = await import(`~/components/${componentName}.vue`)
currentComponent.value = component.default
showComponent.value = true
}
</script>Component Memoization
typescript
// composables/useComponentCache.ts
const componentCache = new Map()
export const useComponentCache = () => {
const getCachedComponent = (key: string) => {
return componentCache.get(key)
}
const setCachedComponent = (key: string, component: any) => {
componentCache.set(key, component)
}
const clearCache = () => {
componentCache.clear()
}
return {
getCachedComponent,
setCachedComponent,
clearCache
}
}Customization
Custom Component Variants
vue
<!-- components/ui/Button.vue -->
<script setup>
interface Props extends BaseProps {
customVariant?: string
}
const props = defineProps<Props>()
const componentClasses = computed(() => [
'btn',
`btn-${props.variant}`,
{
[`btn-custom-${props.customVariant}`]: props.customVariant
}
])
</script>
<style scoped>
.btn-custom-danger {
background-color: #dc2626;
color: white;
}
.btn-custom-danger:hover {
background-color: #b91c1c;
}
</style>Theme Override
vue
<!-- Override component styles globally -->
<style>
/* Override button primary color */
.btn-primary {
background-color: #7c3aed !important;
border-color: #7c3aed !important;
}
[data-theme="dark"] .btn-primary {
background-color: #a855f7 !important;
border-color: #a855f7 !important;
}
</style>Integration with Libraries
Chart.js Integration
vue
<!-- components/charts/LineChart.vue -->
<template>
<div class="chart-container">
<canvas ref="chartRef"></canvas>
</div>
</template>
<script setup>
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
} from 'chart.js'
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
)
interface Props {
data: ChartData
options?: ChartOptions
}
const props = defineProps<Props>()
const chartRef = ref<HTMLCanvasElement>()
onMounted(() => {
if (chartRef.value) {
new ChartJS(chartRef.value, {
type: 'line',
data: props.data,
options: {
responsive: true,
maintainAspectRatio: false,
...props.options
}
})
}
})
</script>
<style scoped>
.chart-container {
position: relative;
height: 400px;
width: 100%;
}
</style>Vue Router Integration
vue
<!-- components/navigation/NavLink.vue -->
<template>
<NuxtLink
:to="to"
:class="linkClasses"
:exact="exact"
v-slot="{ href, navigate, isExactActive }"
>
<a
:href="href"
@click="navigate"
:class="[
linkClasses,
{ 'nav-link-active': isExactActive }
]"
>
<Icon v-if="icon" :name="icon" />
<span><slot /></span>
</a>
</NuxtLink>
</template>
<script setup>
interface Props {
to: string
icon?: string
exact?: boolean
variant?: 'default' | 'primary' | 'secondary'
}
const props = withDefaults(defineProps<Props>(), {
exact: false,
variant: 'default'
})
const linkClasses = computed(() => [
'nav-link',
`nav-link-${props.variant}`
])
</script>Best Practices
Component Naming
typescript
// ✅ Good naming
const components = {
'ui-button': Button,
'ui-card': Card,
'data-table': DataTable,
'nav-sidebar': Sidebar
}
// ❌ Avoid
const components = {
'btn': Button, // Too short
'Button': Button, // PascalCase
'my-button': Button // Prefixed with personal namespace
}Props Design
typescript
// ✅ Good props
interface ButtonProps {
variant: 'primary' | 'secondary' | 'success' | 'warning' | 'error'
size: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
disabled: boolean
loading: boolean
fullWidth: boolean
}
// ❌ Avoid
interface ButtonProps {
type: string // Too generic
style: object // Too broad
config: any // No type safety
}Event Naming
vue
<!-- ✅ Good events -->
<Button
@click="handleClick"
@submit="handleSubmit"
@close="handleClose"
@update:modelValue="handleUpdate"
/>
<!-- ❌ Avoid -->
<Button
@onClick="handleClick" // Redundant prefix
@buttonClick="handleClick" // Component-specific
@change="handleUpdate" // Too generic
/>Migration Guide
From Vue 2 to Vue 3
typescript
// Vue 2 style
export default {
props: {
variant: {
type: String,
default: 'primary'
}
},
data() {
return {
isLoading: false
}
},
methods: {
handleClick() {
this.$emit('click')
}
}
}
// Vue 3 Composition API
interface Props {
variant?: string
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary'
})
const isLoading = ref(false)
const emit = defineEmits<{
click: []
}>()
const handleClick = () => {
emit('click')
}From Options API to Composition API
vue
<!-- Options API -->
<template>
<div>
<input v-model="searchQuery" @input="handleSearch" />
<div v-if="isLoading">Loading...</div>
<div v-for="result in results" :key="result.id">
{{ result.title }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: '',
results: [],
isLoading: false
}
},
methods: {
async handleSearch() {
this.isLoading = true
try {
this.results = await this.$api.search(this.searchQuery)
} finally {
this.isLoading = false
}
}
}
}
</script>
<!-- Composition API -->
<template>
<div>
<input v-model="searchQuery" @input="handleSearch" />
<div v-if="isLoading">Loading...</div>
<div v-for="result in results" :key="result.id">
{{ result.title }}
</div>
</div>
</template>
<script setup>
const searchQuery = ref('')
const results = ref([])
const isLoading = ref(false)
const handleSearch = async () => {
isLoading.value = true
try {
results.value = await $fetch(`/api/search?q=${searchQuery.value}`)
} finally {
isLoading.value = false
}
}
</script>Troubleshooting
Common Issues
Component not rendering:
typescript
// Check if component is properly registered
console.log('Available components:', Object.keys(components))
// Check props
console.log('Component props:', props)Styling issues:
css
/* Check CSS specificity */
.component {
/* Your styles */
}
/* Override with higher specificity if needed */
.app .component {
/* Override styles */
}Performance issues:
typescript
// Use React.memo equivalent
const OptimizedComponent = memo(Component, (prevProps, nextProps) => {
return prevProps.id === nextProps.id
})
// Or use computed for expensive calculations
const expensiveValue = computed(() => {
return heavyCalculation(props.data)
})Contributing
Adding New Components
- Create component file:
bash
mkdir -p components/ui
touch components/ui/NewComponent.vue- Follow naming conventions:
typescript
// components/ui/NewComponent.vue
interface Props {
// Define props
}
const props = withDefaults(defineProps<Props>(), {
// Default values
})
defineEmits<{
// Define events
}>()- Add tests:
typescript
// test/components/ui/NewComponent.test.ts
describe('NewComponent', () => {
test('renders correctly', () => {
// Test implementation
})
})- Update documentation:
markdown
## NewComponent
Description of the new component.
### Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| prop1 | string | '' | Description |
### Events
| Event | Payload | Description |
|-------|---------|-------------|
| event1 | Event | Description |
### Usage
\`\`\`vue
<template>
<NewComponent prop1="value" @event1="handler" />
</template>
\`\`\`Resources
Next: Check out the Examples Section to see components in action!