scaffold-frontend-component
Skill16 starsChanged 3 days agoApache-2.0View on GitHub
Scaffold a new Vue 3 component following project conventions. Supports card, modal/dialog, list/DataTable, form, empty card, and display patterns with PrimeVue, Tailwind, typed props/emits, and i18n. Use when user says "create a component", "add a Vue component", "scaffold a card", "create a modal", "build a DataTable component", "new form component", or "add a dialog for X". Do NOT use for full list pages with card grids (use scaffold-frontend-page), detail pages with tabs (use scaffold-frontend-subpage), composables only (use scaffold-composable), or PrimeVue docs lookup (use primevue-lookup). Generates component file with correct placement, typed props/emits, and dark mode.
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 1 is distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
Read the frontend scope guide: packages/web/CLAUDE.md
- Very specific: line 230
Use v-model pattern: prop modelValue + emit update:modelValue
- Very specific: line 285
Always use size="small" for compact display
- Distinctive: line 295
Every component MUST follow these rules:
- Very specific: line 333
Verify all PrimeVue components used are auto-imported (not in the FormKit-excluded list — see primevue-lookup skill)
- Very specific: line 335
Check dark mode: every bg-*, text-*, border-* class has a dark: variant
- Very specific: line 336
Verify i18n keys exist in ALL 4 locale files for any t('key') calls
- Very specific: line 337
If unsure about PrimeVue component props/events, use mcp__primevue__get_component_props to verify
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Scaffold a New Frontend Component
Generate a new Vue component. Component name and type should be provided via $ARGUMENTS.
Before You Start
- Read the frontend scope guide:
packages/web/CLAUDE.md - Study existing components for the pattern you need:
- Card:
packages/web/components/Agent/Card.vue - Empty card:
packages/web/components/Agent/EmptyCard.vue - Modal:
packages/web/components/Agent/CreateModal.vue - List/Table:
packages/web/components/Thread/List.vue - Display:
packages/web/components/Event/Display/Base.vue
- Card:
Step 1: Determine Component Type
Based on the user's description, identify the component pattern:
| Pattern | When to Use |
| Card | Clickable item in a grid, shows summary info |
| Empty Card | Dashed-border placeholder for "add new" action |
| Modal | Dialog for create/edit forms with v-model visibility |
| List/Table | DataTable with columns, selection, and custom templates |
| Display | Read-only data visualization (charts, stats, details) |
| Form | FormKit-based input form (standalone or inside modal) |
Step 2: Create the Component File
Place in packages/web/components/{Domain}/{ComponentName}.vue.
Naming rules:
- Directory: Domain/resource name in PascalCase (
Agent/,Thread/,Process/) - File: PascalCase descriptive name (
Card.vue,CreateModal.vue,List.vue) - Auto-import name: Directory + File =
AgentCard,ThreadList,ProcessCreateModal
Pattern A: Card Component
<template>
<div
class="flex cursor-pointer flex-col gap-3 rounded-xl border border-surface-200 p-4 hover:bg-surface-100 dark:border-surface-800 hover:dark:bg-surface-800"
:class="{ 'bg-surface-100 dark:bg-surface-800': isActive }"
@click="emit('click', item)"
>
<!-- Header row: icon + title + actions -->
<div class="flex items-center justify-between gap-4">
<div class="flex items-center justify-start gap-2">
<!-- Icon in circular container -->
<div class="flex items-center justify-center rounded-full bg-white p-3 dark:bg-surface-900">
<Icon :name="item.icon ?? 'hugeicons:question'" size="1.5em" />
</div>
<!-- Title + subtitle -->
<div>
<h3 class="font-semibold opacity-80">{{ item.name }}</h3>
<p class="text-xs font-light opacity-70">{{ item.id }}</p>
</div>
</div>
<!-- Status + actions -->
<div class="flex items-center gap-2">
<Tag :value="statusLabel" :severity="statusSeverity" />
<Button
v-if="showDelete"
icon="pi pi-trash"
severity="secondary"
text
rounded
size="small"
:loading="isDeleting"
@click.stop="confirmDelete"
/>
</div>
</div>
<!-- Description -->
<div>
<span class="text-xs">{{ item.description }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import type { <Resource>Dto } from '@core/sdk/client'
const props = withDefaults(defineProps<{
item: <Resource>Dto
showDelete?: boolean
}>(), {
showDelete: true,
})
const emit = defineEmits<{
click: [item: <Resource>Dto]
deleted: [id: string]
}>()
const route = useRoute()
const { t } = useI18n()
const toast = useToast()
const confirm = useConfirm()
const isActive = computed(() => route.params.id === props.item.id)
</script>Card Design Token Reference:
| Element | Classes |
| Container | rounded-xl border border-surface-200 p-4 dark:border-surface-800 |
| Hover | hover:bg-surface-100 hover:dark:bg-surface-800 |
| Active | bg-surface-100 dark:bg-surface-800 |
| Icon container | rounded-full bg-white p-3 dark:bg-surface-900 |
| Title | font-semibold opacity-80 |
| Subtitle | text-xs font-light opacity-70 |
| Description | text-xs |
Pattern B: Empty Card (Add New)
<template>
<div
class="flex cursor-pointer flex-col justify-center gap-3 rounded-xl border-2 border-dashed border-surface-300 p-4 hover:border-primary-500 hover:bg-surface-50 dark:border-surface-600 dark:hover:bg-surface-800"
@click="emit('add')"
>
<div class="flex items-center justify-center">
<div class="flex items-center justify-center p-3">
<i class="pi pi-plus-circle text-surface-400" style="font-size: 1.5rem" />
</div>
</div>
<div class="text-center">
<h3 class="font-medium text-surface-600 dark:text-surface-400">
{{ t('<resource>.add.title') }}
</h3>
<p class="text-sm text-surface-500 dark:text-surface-400">
{{ t('<resource>.add.description') }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
const { t } = useI18n()
const emit = defineEmits<{ add: [] }>()
</script>Empty Card Design Token Reference:
| Element | Classes |
| Container | rounded-xl border-2 border-dashed border-surface-300 p-4 dark:border-surface-600 |
| Hover | hover:border-primary-500 hover:bg-surface-50 dark:hover:bg-surface-800 |
| Icon | pi pi-plus-circle text-surface-400 at 1.5rem |
| Title | font-medium text-surface-600 dark:text-surface-400 |
| Description | text-sm text-surface-500 dark:text-surface-400 |
Pattern C: Modal Component
<template>
<Dialog
v-model:visible="visible"
modal
:header="t('<resource>.create.title')"
:style="{ width: '50rem' }"
>
<!-- Form content -->
<div class="flex flex-col gap-4">
<!-- Form fields using FormKit or PrimeVue inputs -->
</div>
<template #footer>
<Button
:label="t('common.cancel')"
severity="secondary"
text
@click="visible = false"
/>
<Button
:label="t('<resource>.create.submit')"
:loading="isCreating"
@click="handleSubmit"
/>
</template>
</Dialog>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: [id: string]
}>()
const { t } = useI18n()
const { create<Resource>, isCreating } = useCreate<Resource>()
const visible = computed({
get: () => props.modelValue,
set: (value: boolean) => emit('update:modelValue', value),
})
const handleSubmit = async () => {
const result = await create<Resource>({ /* request body */ })
emit('success', result.id)
visible.value = false
}
</script>Modal Pattern Notes:
- Use
v-modelpattern: propmodelValue+ emitupdate:modelValue - Parent uses
<MyModal v-model="isOpen" @success="handleSuccess" /> - PrimeVue
Dialogwithmodalprop for backdrop - Footer buttons: Cancel (secondary, text) + Submit (primary, with loading)
Pattern D: List/Table Component
<template>
<DataTable
:value="items"
size="small"
selection-mode="single"
:selection="selectedItem"
data-key="id"
@update:selection="(item) => emit('selected', item)"
>
<Column field="name" :header="t('<resource>.list.name')" />
<Column field="status" :header="t('<resource>.list.status')">
<template #body="{ data }">
<Tag :value="data.status" :severity="getSeverity(data)" />
</template>
</Column>
<Column field="created_at" :header="t('<resource>.list.created')">
<template #body="{ data }">
{{ formatDate(data.created_at) }}
</template>
</Column>
</DataTable>
</template>
<script setup lang="ts">
import type { <Resource>Dto } from '@core/sdk/client'
const props = defineProps<{
items: <Resource>Dto[]
}>()
const emit = defineEmits<{
selected: [item: <Resource>Dto]
}>()
const { t } = useI18n()
const route = useRoute()
const selectedItem = computed(() => {
return props.items.find(item => item.id === route.params.id)
})
</script>DataTable Pattern Notes:
- Always use
size="small"for compact display selection-mode="single"for row click selectiondata-key="id"for row identity- Custom column templates via
<template #body="{ data }"> - Selection synced with route params via computed
Step 3: Component Conventions Checklist
Every component MUST follow these rules:
<script setup lang="ts">— Composition API only, no Options API- Typed props:
defineProps<{ prop: Type }>()with SDK types from@core/sdk/client - Typed emits:
defineEmits<{ eventName: [arg: Type] }>() - PrimeVue only: No raw
<button>,<input>,<select>— use PrimeVue equivalents - Tailwind only: No custom CSS (exception: scoped
:deep()for PrimeVue overrides) - i18n all text:
{{ t('key.path') }}for user-visible text - Dark mode: Every
bg-*has adark:bg-*variant, everytext-*hasdark:text-* - No unused imports: Components are auto-imported by Nuxt
- Composables for data: Use
defineQuery/defineMutation, never raw fetch useConfirm()for deletions: Always confirm destructive actionsuseToast()for feedback: Success/error messages via toast
File Placement Rules
components/
├── {Domain}/ # Group by domain
│ ├── Card.vue # Resource card for grid layouts
│ ├── EmptyCard.vue # Add-new placeholder
│ ├── CreateModal.vue # Create dialog
│ └── List.vue # DataTable list
├── Structural/ # Layout primitives (DO NOT modify)
│ ├── Screen.vue
│ ├── Column.vue
│ └── Substructure.vue
├── Event/ # Event system
│ └── Display/ # Event display cards
├── Dashboard/ # Dashboard widgets
│ └── Component/ # Widget implementations
├── FormKit/ # Custom FormKit inputs
└── Navigation/ # Nav componentsStep 4: Verify
- Confirm the component auto-import name resolves:
{Domain}/{ComponentName}.vuebecomes{Domain}{ComponentName} - Verify all PrimeVue components used are auto-imported (not in the FormKit-excluded list — see
primevue-lookupskill) - Check dark mode: every
bg-*,text-*,border-*class has adark:variant - Verify i18n keys exist in ALL 4 locale files for any
t('key')calls - If unsure about PrimeVue component props/events, use
mcp__primevue__get_component_propsto verify
Examples
Input: $ARGUMENTS = "Pipeline Card"
Output: packages/web/components/Pipeline/Card.vue -- Pattern A card component with PipelineDto typed props,
click emit, active state from route params, icon + title + status Tag layout.
Input: $ARGUMENTS = "Connector CreateModal"
Output: packages/web/components/Connector/CreateModal.vue -- Pattern C modal with v-model:visible,
useCreateConnector() mutation, cancel/submit footer buttons, and success emit.
Input: $ARGUMENTS = "Knowledge List table"
Output: packages/web/components/Knowledge/List.vue -- Pattern D DataTable with KnowledgeDto[] prop, single
selection mode, custom column templates for status and dates.
Auto-import name resolution: Directory + File = component name. Pipeline/Card.vue becomes <PipelineCard />,
Connector/CreateModal.vue becomes <ConnectorCreateModal />.
Troubleshooting
| Problem | Cause | Fix |
| Component not auto-imported | Wrong directory or naming | Must be under components/ in PascalCase directory, file must be PascalCase .vue |
| Raw HTML elements in template | Not using PrimeVue | Replace button with Button, input with FormKit input, select with FormKit Select |
| Dark mode colors broken | Missing dark: variants | Every bg-*, text-*, border-* class needs a corresponding dark: variant |
| Props not type-safe | Using defineProps({}) object syntax | Use generic syntax: defineProps<{ item: ResourceDto }>() with SDK types |
| FormKit-wrapped component not rendering | Imported from PrimeVue instead of FormKit | 24 components excluded from PrimeVue auto-import — see primevue-lookup skill for full list or check nuxt.config.ts |
| Delete button triggers card click | Missing @click.stop | Add .stop modifier to prevent event bubbling: @click.stop="confirmDelete" |
In this repository50 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- docs/CLAUDE.mdCLAUDE.md
- .github/actions/CLAUDE.mdCLAUDE.md
- arc42Skill
- audit-frontendSkill
- bot-frameworkSkill
- create-or-audit-claude-mdSkill
- create-or-audit-skillSkill
- create-or-audit-subagentSkill
- create-prSkill
- dagster-pipelinesSkill
- debug-agentSkill
- debug-frontendSkill
- debug-pipelineSkill
- design-systemSkill
- docker-devSkill
- document-decisionSkill
- document-featureSkill
- explainSkill
- generate-sdkSkill
- i18nSkill
- implement-feedback-from-prSkill
- likec4-dslSkill
- lintSkill
- merge-mainSkill
- nats-eventsSkill
- plan-issueSkill
- pr-demo-videoSkill
- primevue-lookupSkill
- rclone-guideSkill
- reflectSkill
- review-diffSkill
- scaffold-agentSkill
- scaffold-api-endpointSkill
- scaffold-api-repositorySkill
- scaffold-api-serviceSkill
- scaffold-bot-handlerSkill
- scaffold-composableSkill
- scaffold-event-displaySkill
- scaffold-frontend-pageSkill
- scaffold-frontend-subpageSkill
- scaffold-pipelineSkill
- setup-bot-connectionSkill
- splice-issueSkill
- sync-agent-docsSkill
- test-scopeSkill
- update-docSkill
- verify-sdk-syncSkill
- write-docSkill
- write-issueSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

