npx claudepluginhub bee-coded/bee-dev --plugin beeThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
These standards apply when the project stack is vue. All agents and implementations must follow these conventions.
Also read skills/standards/frontend/SKILL.md for universal frontend standards (component architecture, accessibility, responsive design, CSS methodology, design quality) that apply alongside these Vue-specific conventions.
.vue file with <script setup lang="ts">, <template>, and optionally <style scoped>.<script setup> is the ONLY accepted syntax. NEVER use Options API. NEVER use a plain <script> block with export default defineComponent() unless you need inheritAttrs: false (use a separate <script> block for that single option only).defineProps with TypeScript generics. Provide defaults via withDefaults. Keep prop interfaces narrow.Tabs/TabList.vue, Tabs/TabPanel.vue, Tabs/index.ts).<script setup> first, then <template>, then <style scoped>. Always follow this order.<!-- Pattern: component with props, emits, slots, and composable -->
<script setup lang="ts">
import { computed } from 'vue'
import { useOrders } from '@/composables/useOrders'
interface Props {
customerId: string
limit?: number
}
const props = withDefaults(defineProps<Props>(), {
limit: 10,
})
const emit = defineEmits<{
select: [orderId: string]
}>()
const { orders, loading, error } = useOrders(props.customerId, props.limit)
const totalValue = computed(() =>
orders.value.reduce((sum, o) => sum + o.total, 0)
)
</script>
<template>
<div>
<slot name="header" :total="totalValue" />
<ul v-if="!loading">
<li v-for="order in orders" :key="order.id" @click="emit('select', order.id)">
{{ order.name }} -- {{ order.total }}
</li>
</ul>
<p v-else>Loading...</p>
<slot name="footer" />
</div>
</template>
ref() for all reactive values -- primitives and objects. Prefer ref over reactive for consistency and clarity.reactive() for deeply nested objects only when you need automatic unwrapping. Use sparingly -- ref() is the default choice.computed() for derived values that depend on reactive state. NEVER store derived values in a separate ref and sync them with watch.toRefs() / toRef() to destructure reactive objects without losing reactivity. Use when spreading props or reactive objects.shallowRef() / shallowReactive() for large objects where deep reactivity is unnecessary (e.g., large lists, third-party object instances).watch() for explicit side effects on specific sources. Always provide a source (ref, getter, or array of sources).watchEffect() for side effects that auto-track their dependencies. Runs immediately on creation.watchPostEffect() when you need to access updated DOM after a reactive change.onCleanup parameter in watch / watchEffect callbacks for abort controllers, timers, and subscriptions.once: true option (Vue 3.4+) for watchers that should fire only once.// Pattern: watch with cleanup and abort
watch(searchQuery, async (query, _oldQuery, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort())
loading.value = true
try {
results.value = await fetchResults(query, { signal: controller.signal })
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') return
error.value = e as Error
} finally {
loading.value = false
}
})
onMounted() for DOM access, third-party library initialization, initial data fetching.onUnmounted() for cleanup: remove event listeners, clear timers, abort requests, disconnect observers.onBeforeUnmount() for cleanup that must happen before DOM removal.onUpdated() -- use sparingly. Prefer watch or watchEffect for reactive side effects.onActivated() / onDeactivated() for components inside <KeepAlive>.onMounted adds a listener, onUnmounted removes it.defineProps<T>() with a TypeScript interface for type-safe props.withDefaults() for default values when using the type-only syntax.<script setup lang="ts">
interface Props {
title: string
items: Item[]
variant?: 'primary' | 'secondary'
onAction?: (id: string) => void
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
})
</script>
defineEmits<T>() with typed event signatures. Use the tuple syntax for payload types.const emit = defineEmits<{
update: [id: number, value: string]
delete: [id: number]
close: []
}>()
defineModel() for two-way binding (v-model support). Replaces the modelValue prop + update:modelValue emit pattern.// Parent: <ToggleSwitch v-model="isEnabled" v-model:label="labelText" />
const isEnabled = defineModel<boolean>({ required: true })
const label = defineModel<string>('label', { default: 'Toggle' })
defineExpose() to explicitly expose properties to parent template refs. By default, <script setup> components expose nothing.defineExpose({
reset: () => { formData.value = initialState },
validate: () => schema.safeParse(formData.value),
})
defineSlots<T>() for typed slot props. Enables type checking on scoped slot content.defineSlots<{
default: (props: { item: Item; index: number }) => any
header: (props: { total: number }) => any
empty: () => any
}>()
generic attribute on <script setup> for reusable generic components.<script setup lang="ts" generic="T extends { id: string | number }">
defineProps<{
items: T[]
selected?: T
}>()
defineEmits<{
select: [item: T]
}>()
</script>
use: useAuth(), useFilters(), usePagination(), useDebounce().src/composables/. Feature-specific composables colocate with the feature.MaybeRef<T> or MaybeRefOrGetter<T> for composable parameters so callers can pass either raw values or refs.onUnmounted or watcher cleanup.// Pattern: composable with ref input, cleanup, and abort
import { ref, watch, onUnmounted, type MaybeRefOrGetter, toValue } from 'vue'
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = ref<T | null>(null)
const loading = ref(true)
const error = ref<Error | null>(null)
let controller: AbortController | null = null
const fetchData = async () => {
controller?.abort()
controller = new AbortController()
loading.value = true
error.value = null
try {
const res = await fetch(toValue(url), { signal: controller.signal })
data.value = await res.json()
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') return
error.value = e as Error
} finally {
loading.value = false
}
}
watch(() => toValue(url), fetchData, { immediate: true })
onUnmounted(() => controller?.abort())
return { data, loading, error, refetch: fetchData }
}
ref() for component-level state. Keep state as close to where it is used as possible.Detect what the project uses -- check package.json for installed state management libraries and follow THAT library's conventions. Do NOT introduce a different state library than what the project already uses.
<script setup>.src/stores/ with use*Store naming: useAuthStore.ts, useCartStore.ts.ref() values. Getters: computed() values. Actions: plain functions. All must be returned.storeToRefs() when destructuring store state to maintain reactivity: const { count, name } = storeToRefs(store).$reset() for resetting state (define it manually in setup stores).$subscribe() for reacting to state changes outside components.// Pattern: Pinia setup store with TypeScript
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const loading = ref(false)
const isAuthenticated = computed(() => !!token.value)
const displayName = computed(() => user.value?.name ?? 'Guest')
async function login(credentials: LoginCredentials) {
loading.value = true
try {
const res = await api.post<AuthResponse>('/auth/login', credentials)
user.value = res.data.user
token.value = res.data.token
} finally {
loading.value = false
}
}
function logout() {
user.value = null
token.value = null
}
function $reset() {
user.value = null
token.value = null
loading.value = false
}
return { user, token, loading, isAuthenticated, displayName, login, logout, $reset }
})
ref() at module scope act as simple global stores.Key rule: Match the project. If the project uses Pinia, write Pinia. If it uses Vuex, write Vuex. Never mix state libraries without explicit user direction.
createRouter with createWebHistory for HTML5 history mode (recommended) or createWebHashHistory for hash mode.src/router/index.ts file.RouteMeta.// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
}
}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{ path: '', name: 'home', component: () => import('@/pages/HomePage.vue') },
{
path: 'orders',
name: 'orders',
component: () => import('@/pages/OrdersPage.vue'),
meta: { requiresAuth: true },
},
{
path: 'orders/:id',
name: 'order-detail',
component: () => import('@/pages/OrderDetailPage.vue'),
meta: { requiresAuth: true },
},
],
},
{ path: '/login', name: 'login', component: () => import('@/pages/LoginPage.vue') },
{ path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/pages/NotFoundPage.vue') },
],
})
beforeEach for authentication checks, page titles, and analytics.beforeEnter for route-specific authorization.onBeforeRouteLeave and onBeforeRouteUpdate composables from vue-router.beforeEach, redirect to login with return URL.router.beforeEach(async (to, from) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } }
}
})
router.afterEach((to) => {
document.title = to.meta.title || 'My App'
})
useRouter() for programmatic navigation: router.push(), router.replace(), router.back().useRoute() for reading current route params, query, and meta. Access is reactive.<RouterLink> for declarative navigation in templates. Use active-class for styling.() => import(...)) for route components. Vite handles code splitting automatically.<RouterView> in parent layout components for child route rendering.<!-- Pattern: in-component guard for unsaved changes -->
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
const hasUnsavedChanges = ref(false)
onBeforeRouteLeave(() => {
if (hasUnsavedChanges.value) {
const confirm = window.confirm('You have unsaved changes. Leave anyway?')
if (!confirm) return false
}
})
</script>
v-model for two-way binding on native inputs and custom components.v-model with modifiers: .trim, .number, .lazy for built-in transformations.v-model:title="title" v-model:content="content" on custom components using defineModel().:value + @input when you need to intercept or transform input.useForm(), useField(), and <Form> / <Field> components.z.infer<typeof schema>.@vee-validate/zod adapter with toTypedSchema() for integration.<script setup lang="ts">
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
})
type FormData = z.infer<typeof schema>
const { handleSubmit, errors, defineField } = useForm<FormData>({
validationSchema: toTypedSchema(schema),
})
const [email, emailAttrs] = defineField('email')
const [password, passwordAttrs] = defineField('password')
const onSubmit = handleSubmit(async (values) => {
await api.post('/auth/login', values)
})
</script>
<template>
<form @submit.prevent="onSubmit">
<input v-model="email" v-bind="emailAttrs" type="email" />
<span v-if="errors.email">{{ errors.email }}</span>
<input v-model="password" v-bind="passwordAttrs" type="password" />
<span v-if="errors.password">{{ errors.password }}</span>
<button type="submit">Log in</button>
</form>
</template>
useLoginForm(), useOrderForm().@vitejs/plugin-vue plugin.VITE_ prefix. Access via import.meta.env.VITE_API_URL. Never expose secrets without prefix.vite.config.ts under resolve.alias (e.g., @/ maps to src/). Mirror in tsconfig.json paths.server.proxy in Vite config for API calls to avoid CORS during development.defineAsyncComponent() or route-level lazy loading for splitting.build.rollupOptions.output.manualChunks for vendor splitting when needed..env, .env.local, .env.production, .env.staging -- loaded based on --mode flag.// vite.config.ts -- typical Vue project configuration
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
})
mount() / shallowMount() from Vue Test Utils, or render() / screen from Vue Testing Library.vi.mock() for fetch/axios.withSetup() helper.// Pattern: component test with Vue Test Utils
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import OrderList from '@/components/OrderList.vue'
test('renders orders and handles selection', async () => {
const wrapper = mount(OrderList, {
props: { customerId: '123' },
global: {
plugins: [createTestingPinia({
initialState: { orders: { items: mockOrders } },
})],
},
})
expect(wrapper.findAll('li')).toHaveLength(mockOrders.length)
await wrapper.find('li').trigger('click')
expect(wrapper.emitted('select')?.[0]).toEqual(['order-1'])
})
// Pattern: composable test
import { useFetch } from '@/composables/useFetch'
import { flushPromises } from '@vue/test-utils'
test('fetches data and exposes reactive state', async () => {
const { data, loading, error } = withSetup(() => useFetch<User[]>('/api/users'))
expect(loading.value).toBe(true)
await flushPromises()
expect(loading.value).toBe(false)
expect(data.value).toHaveLength(3)
expect(error.value).toBeNull()
})
InjectionKey<T> for type safety.// keys.ts
import type { InjectionKey } from 'vue'
export const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('Theme')
export const ApiClientKey: InjectionKey<ApiClient> = Symbol('ApiClient')
// Provider component
import { provide, ref } from 'vue'
const theme = ref<'light' | 'dark'>('light')
provide(ThemeKey, theme)
// Consumer composable (recommended pattern)
export function useTheme() {
const theme = inject(ThemeKey)
if (!theme) throw new Error('useTheme must be used within a ThemeProvider')
return theme
}
<Teleport to="body"> for modals, toasts, and tooltips that need to render outside the component tree.disabled prop to conditionally disable teleporting (e.g., render inline on mobile).<Suspense> to handle async components with #default and #fallback slots.await in <script setup>) in Suspense.onErrorCaptured or an <ErrorBoundary> component for error handling.<template>
<Suspense>
<template #default>
<AsyncDashboard />
</template>
<template #fallback>
<LoadingSkeleton />
</template>
</Suspense>
</template>
<KeepAlive> to cache component instances when toggling between them (tabs, wizard steps).include / exclude props to control which components are cached by name.max to limit cached instances and prevent memory leaks.onActivated() / onDeactivated() lifecycle hooks for setup/teardown in kept-alive components.<KeepAlive :include="['OrderList', 'UserProfile']" :max="5">
<component :is="currentTab" />
</KeepAlive>
<script setup> with Composition API.defineModel() for two-way binding.reactive() for primitive values -- use ref(). Reassigning a reactive variable loses reactivity.toRefs() or toRef() -- destructured values lose reactivity..value when accessing refs in <script setup> -- templates auto-unwrap, but script does not.v-if and v-for on the same element -- v-if has higher priority in Vue 3 and cannot access the v-for scope variable. Wrap with <template v-for> and put v-if on the child.v-for lists that reorder or mutate -- use a stable unique identifier.ref and sync it with watch -- use computed() instead.any type in TypeScript -- define proper interfaces, generics, or unknown.import.meta.env without VITE_ prefix -- unprefixed vars are not available client-side, but setting envPrefix to empty string is a security risk.onMounted -- if you add event listeners, timers, or observers, remove them in onUnmounted.this in <script setup> -- there is no component instance. All state is accessed directly via refs and composables.<script setup> or inside other composables, just like React hooks.$refs, $emit, $slots via the Options API style -- use useTemplateRef(), defineEmits(), and useSlots() instead..js files in the source tree.<script setup> only. Every Vue component uses <script setup lang="ts">. No Options API, no defineComponent() blocks.use* prefix). Components orchestrate composables, not contain raw logic.v-for lists. Every list element has a stable, unique :key derived from domain data -- never from array index in dynamic lists.onMounted with listeners, timers, or async must have a corresponding onUnmounted cleanup.defineProps<T>(). All component props are typed via TypeScript generics, never runtime-only prop validation.shallowRef for large collections. Use shallowRef for lists of hundreds+ items where deep reactivity is unnecessary.computed for derived data. NEVER store derived values in separate refs and sync with watchers.package.json first -- follow established patterns, don't introduce new libraries.v-memo for expensive list rendering. Use v-memo on v-for items to skip re-rendering unchanged items in large lists.InjectionKey<T> for type-safe dependency injection.<style scoped> to prevent CSS leakage. Use :deep() only when targeting child component internals.defineAsyncComponent. Use for heavy components not needed at initial render (charts, editors, maps).watchEffect for auto-tracking. Use watchEffect when the callback reads multiple reactive sources -- no need to list dependencies manually.reactive() or props loses reactivity. Use toRefs(props) or access as props.name..value in script. Accessing a ref without .value in <script setup> returns the ref object, not the value. Templates auto-unwrap -- script does not.v-if + v-for on same element. In Vue 3, v-if is evaluated first and cannot access v-for scope. Move v-for to a <template> wrapper.defineModel().v-for. No key or unstable key causes DOM thrashing and lost component state.reactive() variable (state = newObj) replaces the reference and breaks reactivity. Use ref() or Object.assign().const val = ref.value before async) reads stale data. Always read .value at the point of use.<Teleport to="#modal-root"> when the target element doesn't exist causes a runtime warning and the content doesn't render.deep. Watching a reactive() object detects only reassignment by default. Use { deep: true } or watch specific properties via getter.<script setup>, composables, or modern TypeScript integration. Always use Composition API.any type. Disables type checking and hides bugs. Use proper interfaces, generics, or unknown.computed().$forceUpdate() / direct DOM manipulation. Let Vue's reactivity system handle updates. If you need $forceUpdate, your data flow is broken.watch and watchEffect. Most "reactive side effects" can be expressed as computed values or template bindings. Only use watchers for true side effects (API calls, analytics, logging).OrderList.vue, UserProfile.vue, DefaultLayout.vue.use prefix. useAuth.ts, usePagination.ts, useDebounce.ts.formatDate.ts, apiClient.ts, validators.ts.index.ts. Internal details not re-exported.OrderList.vue and OrderList.test.ts in the same directory..vue file.@/ for all imports from src/. Never use relative paths that climb more than one level.use*Store. useAuthStore.ts, useCartStore.ts, useOrderStore.ts.src/
features/
orders/
OrderList.vue
OrderList.test.ts
OrderDetail.vue
useOrders.ts
orderApi.ts
index.ts
auth/
LoginForm.vue
useAuth.ts
authApi.ts
index.ts
composables/ (shared composables)
components/ (shared UI components)
stores/ (Pinia stores)
router/ (router config and guards)
lib/ (utilities, api client, constants)
layouts/ (layout components)
pages/ (route-level page components)
When looking up framework documentation, use these Context7 library identifiers:
/vuejs/docs -- Composition API, reactivity, components, script setup, TypeScript, built-in components/vuejs/router -- routing, guards, navigation, nested routes, lazy loading, Composition API/vuejs/pinia -- stores, state, getters, actions, plugins, TypeScript, setup stores/websites/vite_dev -- configuration, build, environment variables, plugins, dev servervitest-dev/vitest -- test runner, assertions, mocking, configurationvee-validate -- form validation, useForm, useField, Zod integrationAlways check Context7 for the latest API when working with Vue 3.5 features (defineModel, defineSlots, generic components, Suspense). Training data may be outdated.