Vue Generator -- Memoire Spec to Vue SFC
Transforms Memoire component specs into production-ready Vue 3 Single File Components. Uses Composition API with <script setup>, TypeScript, Tailwind CSS, and composable patterns for state management.
Atomic Design Mapping to Vue
| Atomic Level | Vue Pattern | Output Directory | Example |
|---|---|---|---|
| atom | SFC, no composables | components/ui/ |
MButton.vue, MBadge.vue |
| molecule | SFC, may use atoms | components/molecules/ |
SearchField.vue, UserChip.vue |
| organism | SFC + composable | components/organisms/ |
DataTable.vue, Sidebar.vue |
| template | SFC (slot-based layout) | components/templates/ |
DashboardLayout.vue |
| page | SFC + Pinia store | pages/ |
HomePage.vue, SettingsPage.vue |
Rules
- Atoms are self-contained. No imports from other components. Props in, events out.
- Molecules compose 2-5 atom components. Keep logic minimal -- presentation layer only.
- Organisms use composables for complex state. May fetch data via composables or stores.
- Templates use named slots for content areas. Never contain business logic or real data.
- Pages connect templates to Pinia stores and route params.
Script Setup with TypeScript
All generated components use <script setup lang="ts">. Define props and emits with full type safety.
Props
<script setup lang="ts">
interface Props {
label: string
variant?: 'primary' | 'secondary' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false,
})
</script>
Emits
<script setup lang="ts">
interface Emits {
(e: 'click', event: MouseEvent): void
(e: 'update:modelValue', value: string): void
}
const emit = defineEmits<Emits>()
</script>
Slots
<script setup lang="ts">
defineSlots<{
default(): any
icon?(): any
description?(): any
}>()
</script>
Expose
Only expose what is needed for parent component refs:
<script setup lang="ts">
const inputRef = ref<HTMLInputElement | null>(null)
function focus() {
inputRef.value?.focus()
}
defineExpose({ focus })
</script>
Tailwind / UnoCSS Styling
Class Organization
Follow this order within class attributes:
1. Layout (flex, grid, block, inline)
2. Positioning (relative, absolute, top-0)
3. Box model (w-full, h-12, p-4, m-2)
4. Typography (text-sm, font-medium, text-gray-700)
5. Visual (bg-white, border, rounded-lg, shadow)
6. Interactive (hover:, focus:, active:, transition)
Token-Aware Classes
Map design tokens via Tailwind config extensions:
<template>
<button
class="inline-flex items-center justify-center
h-10 px-4
text-sm font-medium text-primary
bg-bg-primary border border-border-default rounded-default
hover:bg-bg-secondary focus:outline-none focus:ring-2 focus:ring-accent
transition-colors duration-150"
>
<slot />
</button>
</template>
Dynamic Classes
Use computed properties for variant-driven styling:
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
variant?: 'primary' | 'secondary' | 'ghost'
}
const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
})
const variantClasses = computed(() => {
const map: Record<string, string> = {
primary: 'bg-accent text-white hover:bg-accent/90',
secondary: 'bg-bg-secondary text-text-primary hover:bg-bg-secondary/80',
ghost: 'bg-transparent text-text-primary hover:bg-bg-secondary',
}
return map[props.variant]
})
</script>
Composable Patterns for State
Naming Convention
All composables start with use and live in composables/.
Data Fetching Composable
// composables/useUsers.ts
import { ref, onMounted } from 'vue'
import type { User } from '@/types'
export function useUsers() {
const users = ref<User[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchUsers() {
loading.value = true
error.value = null
try {
const response = await fetch('/api/users')
users.value = await response.json()
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
}
onMounted(fetchUsers)
return { users, loading, error, refresh: fetchUsers }
}
Toggle Composable
// composables/useToggle.ts
import { ref } from 'vue'
export function useToggle(initial = false) {
const state = ref(initial)
function toggle() { state.value = !state.value }
function setTrue() { state.value = true }
function setFalse() { state.value = false }
return { state, toggle, setTrue, setFalse }
}
Form Composable
// composables/useForm.ts
import { reactive, computed } from 'vue'
export function useForm<T extends Record<string, any>>(initialValues: T) {
const values = reactive({ ...initialValues }) as T
const errors = reactive<Partial<Record<keyof T, string>>>({})
const dirty = computed(() =>
Object.keys(initialValues).some(
(key) => values[key as keyof T] !== initialValues[key as keyof T]
)
)
function reset() {
Object.assign(values, initialValues)
Object.keys(errors).forEach((key) => delete errors[key as keyof T])
}
function setError(field: keyof T, message: string) {
errors[field] = message
}
return { values, errors, dirty, reset, setError }
}
Pinia Store Generation
Store Template
// stores/users.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'
export const useUsersStore = defineStore('users', () => {
// State
const users = ref<User[]>([])
const loading = ref(false)
const selectedId = ref<string | null>(null)
// Getters
const selectedUser = computed(() =>
users.value.find((u) => u.id === selectedId.value) ?? null
)
const userCount = computed(() => users.value.length)
// Actions
async function fetchUsers() {
loading.value = true
try {
const response = await fetch('/api/users')
users.value = await response.json()
} finally {
loading.value = false
}
}
function selectUser(id: string) {
selectedId.value = id
}
return { users, loading, selectedId, selectedUser, userCount, fetchUsers, selectUser }
})
Store Mapping from Spec
| Spec Field | Pinia Concept | Notes |
|---|---|---|
state properties |
ref() in setup |
Each state field becomes a ref |
computed properties |
computed() |
Derived values |
actions |
Functions | Async operations, mutations |
subscriptions |
$subscribe |
Watch for external changes |
Example: Spec to Vue SFC Output
Input Spec (JSON)
{
"name": "StatusBadge",
"atomicLevel": "atom",
"description": "Colored badge showing status text",
"props": [
{ "name": "label", "type": "string", "required": true },
{ "name": "variant", "type": "enum", "values": ["success", "warning", "error", "info"], "default": "info" }
],
"tokens": {
"borderRadius": "radius.sm",
"paddingX": "spacing.2",
"paddingY": "spacing.1",
"fontSize": "text.xs"
}
}
Output Vue SFC
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
label: string
variant?: 'success' | 'warning' | 'error' | 'info'
}
const props = withDefaults(defineProps<Props>(), {
variant: 'info',
})
const variantClasses = computed(() => {
const map: Record<string, string> = {
success: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
warning: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
error: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
info: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
}
return map[props.variant]
})
</script>
<template>
<span
:class="[
'inline-flex items-center px-2 py-0.5 text-xs font-medium rounded-sm',
variantClasses,
]"
>
{{ label }}
</span>
</template>
File Generation Conventions
| Artifact | Naming | Location |
|---|---|---|
| Component SFC | PascalCase.vue |
components/{level}/ |
| Composable | useCamelCase.ts |
composables/ |
| Pinia store | camelCase.ts |
stores/ |
| Type definitions | camelCase.ts |
types/ |
| Test file | PascalCase.spec.ts |
__tests__/ or co-located |
Codegen Checklist
- Read spec JSON and validate with Zod schema
- Determine atomic level and output directory
- Generate
<script setup lang="ts">with typed props, emits, and slots - Map design tokens to Tailwind utility classes
- Generate template with semantic HTML and conditional classes
- Generate composable if spec declares state management needs
- Generate Pinia store if spec declares shared/global state
- Generate test file with Vitest and Vue Test Utils
- Update barrel export (
index.ts) in the component directory - Run
eslint --fixandvue-tsc --noEmiton output