jan-blonde/jan-blonde icon
public
Published on 5/2/2025
jan-blonde/jan-blonde

Vue+Tailwind Coding guidelines

Rules

Vue + Tailwind AI Assistant Coding Guidelines

Core Technologies

  • Vue 3 with Composition API as the foundation framework
  • Tailwind CSS for styling
  • TypeScript for type safety

Architecture & Patterns

Component Structure

  • Use Vue 3's Composition API with <script setup> syntax for all components
  • Follow a clear folder structure:
    /src
      /assets
      /components
        /ui          # Reusable UI components
        /layouts     # Layout components
        /features    # Feature-specific components
      /composables   # Custom composables
      /stores        # Pinia stores
      /services      # API services
      /utils         # Utility functions
      /types         # TypeScript type definitions
      /views         # Page components
    

Component Design

  • Create atomic, single-responsibility components
  • Use props validation with TypeScript interfaces
  • Implement slots for flexible content insertion
  • Use provide/inject for deep component communication where props would be cumbersome

UI Development Guidelines

Tailwind Usage

  • Use Tailwind utility classes directly in templates
  • Extract common patterns to components, not custom CSS classes
  • Maintain consistent spacing using Tailwind's spacing scale
  • Follow mobile-first responsive design principles
  • Use Tailwind's color palette consistently and avoid custom colors when possible

Example Component Pattern

<template>
  <div class="rounded-lg p-4 shadow-md bg-white dark:bg-gray-800">
    <div class="flex items-center justify-between mb-4">
      <h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ title }}</h3>
      <button v-if="dismissable" 
              @click="$emit('dismiss')" 
              class="text-gray-400 hover:text-gray-500 transition-colors">
        <span class="sr-only">Dismiss</span>
        <XMarkIcon class="h-5 w-5" />
      </button>
    </div>
    
    <div class="text-gray-700 dark:text-gray-300">
      <slot></slot>
    </div>
    
    <div v-if="$slots.footer" class="mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script setup lang="ts">
import { XMarkIcon } from '@heroicons/vue/24/outline'

interface Props {
  title: string
  dismissable?: boolean
}

defineProps<Props>()
defineEmits<{
  (e: 'dismiss'): void
}>()
</script>

State Management with Pinia

Store Structure

  • Create modular stores for different domains of your application
  • Use TypeScript interfaces for strict typing
  • Implement clear actions, getters, and state

Example Store

// stores/aiAssistant.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Message, AssistantState } from '@/types'
import { fetchCompletion } from '@/services/ai'

export const useAIAssistantStore = defineStore('aiAssistant', () => {
  // State
  const messages = ref<Message[]>([])
  const isProcessing = ref(false)
  const error = ref<string | null>(null)
  
  // Getters
  const conversationHistory = computed(() => messages.value)
  const lastMessage = computed(() => 
    messages.value.length > 0 ? messages.value[messages.value.length - 1] : null
  )
  
  // Actions
  async function sendMessage(content: string) {
    try {
      isProcessing.value = true
      error.value = null
      
      // Add user message
      messages.value.push({
        role: 'user',
        content,
        timestamp: new Date()
      })
      
      // Get AI response
      const response = await fetchCompletion(messages.value)
      
      // Add AI response
      messages.value.push({
        role: 'assistant',
        content: response.content,
        timestamp: new Date()
      })
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'An unknown error occurred'
    } finally {
      isProcessing.value = false
    }
  }
  
  function clearConversation() {
    messages.value = []
  }
  
  return { 
    // State
    messages, 
    isProcessing,
    error,
    
    // Getters
    conversationHistory,
    lastMessage,
    
    // Actions
    sendMessage,
    clearConversation
  }
})

API Integration

Service Pattern

  • Create service modules for API integration
  • Use axios or fetch with proper error handling
  • Implement retry logic for network failures

Example Service

// services/ai.ts
import axios from 'axios'
import type { Message } from '@/types'

const API_URL = import.meta.env.VITE_AI_API_URL
const API_KEY = import.meta.env.VITE_AI_API_KEY

export async function fetchCompletion(messages: Message[]) {
  try {
    const response = await axios.post(`${API_URL}/completions`, {
      messages: messages.map(({ role, content }) => ({ role, content })),
      temperature: 0.7,
      max_tokens: 1000,
    }, {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${API_KEY}`
      }
    })
    
    return response.data
  } catch (error) {
    console.error('AI completion error:', error)
    throw new Error('Failed to get AI response')
  }
}

Form Handling with VeeValidate + Zod

Form Implementation

  • Use VeeValidate for form management
  • Use Zod for schema validation
  • Create reusable form components

Example Form Component

<template>
  <form @submit="onSubmit" class="space-y-4">
    <div>
      <label for="message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
        Your message
      </label>
      <textarea
        id="message"
        v-model="message"
        :disabled="isSubmitting"
        class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"
        rows="4"
        placeholder="Type your message here..."
      ></textarea>
      <p v-if="errors.message" class="mt-1 text-sm text-red-600 dark:text-red-400">
        {{ errors.message }}
      </p>
    </div>
    
    <div class="flex justify-end">
      <button
        type="submit"
        :disabled="isSubmitting"
        class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        <span v-if="isSubmitting">
          <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" 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>
          Sending...
        </span>
        <span v-else>Send</span>
      </button>
    </div>
  </form>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from 'vee-validate'
import { z } from 'zod'
import { toFormValidator } from '@vee-validate/zod'
import { useAIAssistantStore } from '@/stores/aiAssistant'

const aiStore = useAIAssistantStore()

// Define validation schema with Zod
const validationSchema = toFormValidator(
  z.object({
    message: z.string().min(1, 'Please enter a message').max(1000, 'Message is too long')
  })
)

// Use VeeValidate's useForm
const { handleSubmit, errors } = useForm({
  validationSchema
})

const message = ref('')
const isSubmitting = ref(false)

// Form submission handler
const onSubmit = handleSubmit(async (values) => {
  try {
    isSubmitting.value = true
    await aiStore.sendMessage(message.value)
    message.value = '' // Clear input after successful submission
  } catch (error) {
    console.error('Error sending message:', error)
  } finally {
    isSubmitting.value = false
  }
})
</script>

Data Fetching with Vue Query (TanStack Query)

Query Pattern

  • Implement Vue Query for complex data fetching scenarios
  • Use query keys for caching and invalidation
  • Handle loading, error, and success states

Example Query Hook

// composables/useAICompletion.ts
import { useQuery, useMutation } from '@tanstack/vue-query'
import { fetchCompletion } from '@/services/ai'
import type { Message } from '@/types'

export function useAICompletion() {
  const messages = ref<Message[]>([])
  
  // Mutation for sending messages
  const sendMessageMutation = useMutation({
    mutationFn: (content: string) => {
      const newMessages = [...messages.value, {
        role: 'user',
        content,
        timestamp: new Date()
      }]
      
      return fetchCompletion(newMessages)
    },
    onSuccess: (data, variables) => {
      // Add user message
      messages.value.push({
        role: 'user',
        content: variables,
        timestamp: new Date()
      })
      
      // Add AI response
      messages.value.push({
        role: 'assistant',
        content: data.content,
        timestamp: new Date()
      })
    }
  })
  
  return {
    messages,
    sendMessage: sendMessageMutation.mutate,
    isPending: sendMessageMutation.isPending,
    error: sendMessageMutation.error
  }
}

Accessibility Guidelines

  • Ensure proper contrast ratios for text using Tailwind's color system
  • Use semantic HTML elements
  • Implement proper ARIA attributes
  • Ensure keyboard navigation works properly
  • Test with screen readers

Performance Optimization

  • Use Vue's built-in features for lazy loading components
  • Implement proper memoization using computed and watchEffect
  • Optimize re-renders with v-once and v-memo where appropriate
  • Use shallowRef for large objects that don't need deep reactivity

Testing Strategy

  • Unit test components with Vitest
  • Use Vue Test Utils for component testing
  • Implement Cypress for E2E testing
  • Test API integrations with MSW (Mock Service Worker)

Code Quality Tools

  • ESLint with Vue plugin for code linting
  • Prettier for code formatting
  • TypeScript for static type checking
  • Husky for pre-commit hooks

AI Integration Best Practices

  • Implement proper error handling for AI service failures
  • Use streaming responses when available
  • Maintain conversation history efficiently
  • Implement rate limiting and user feedback
  • Consider implementing local fallbacks for common queries

Example AI Chat Component

<template>
  <div class="flex flex-col h-full">
    <!-- Chat Messages -->
    <div class="flex-1 overflow-y-auto p-4 space-y-4" ref="messagesContainer">
      <template v-if="messages.length === 0">
        <div class="text-center py-8 text-gray-500 dark:text-gray-400">
          <ChatBubbleLeftIcon class="h-12 w-12 mx-auto mb-4 opacity-50" />
          <p>No messages yet. Start a conversation!</p>
        </div>
      </template>
      
      <div v-for="(message, index) in messages" :key="index" class="flex"
          :class="message.role === 'user' ? 'justify-end' : 'justify-start'">
        <div class="max-w-3/4 rounded-lg px-4 py-2"
            :class="message.role === 'user' 
              ? 'bg-indigo-100 text-indigo-900 dark:bg-indigo-900 dark:text-indigo-100' 
              : 'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100'">
          <div class="prose dark:prose-invert" v-html="formatMessage(message.content)"></div>
        </div>
      </div>
      
      <div v-if="isProcessing" class="flex justify-start">
        <div class="max-w-3/4 rounded-lg px-4 py-2 bg-gray-100 dark:bg-gray-800">
          <div class="flex items-center space-x-2">
            <div class="w-2 h-2 rounded-full bg-gray-400 animate-pulse"></div>
            <div class="w-2 h-2 rounded-full bg-gray-400 animate-pulse delay-75"></div>
            <div class="w-2 h-2 rounded-full bg-gray-400 animate-pulse delay-150"></div>
          </div>
        </div>
      </div>
    </div>
    
    <!-- Input Form -->
    <div class="border-t border-gray-200 dark:border-gray-700 p-4">
      <form @submit.prevent="sendMessage" class="flex space-x-2">
        <textarea
          v-model="newMessage"
          @keydown.enter.prevent="sendMessage"
          class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"
          placeholder="Type your message..."
          rows="1"
          :disabled="isProcessing"
        ></textarea>
        <button
          type="submit"
          :disabled="isProcessing || !newMessage.trim()"
          class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
        >
          <PaperAirplaneIcon class="h-5 w-5" />
        </button>
      </form>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onUpdated, nextTick } from 'vue'
import { ChatBubbleLeftIcon, PaperAirplaneIcon } from '@heroicons/vue/24/outline'
import { useAIAssistantStore } from '@/stores/aiAssistant'
import { marked } from 'marked'
import DOMPurify from 'dompurify'

const aiStore = useAIAssistantStore()
const messages = computed(() => aiStore.conversationHistory)
const isProcessing = computed(() => aiStore.isProcessing)
const newMessage = ref('')
const messagesContainer = ref<HTMLElement | null>(null)

// Scroll to bottom when new messages are added
onUpdated(() => {
  nextTick(() => {
    if (messagesContainer.value) {
      messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
    }
  })
})

// Format message content with markdown
function formatMessage(content: string): string {
  return DOMPurify.sanitize(marked(content))
}

// Send a new message
async function sendMessage() {
  if (!newMessage.value.trim() || isProcessing.value) return
  
  const message = newMessage.value
  newMessage.value = ''
  
  await aiStore.sendMessage(message)
}
</script>

Deployment & CI/CD

  • Use GitHub Actions for CI/CD pipeline
  • Implement staging and production environments
  • Use Vercel or Netlify for frontend deployment
  • Implement proper environment variable management