agentleFS
bbvch-aibbvch-ai/aihub-core/.claude/skills/scaffold-frontend-component/SKILL.md

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
  1. Very specific:

    Read the frontend scope guide: packages/web/CLAUDE.md

    line 13
  2. Very specific:

    Use v-model pattern: prop modelValue + emit update:modelValue

    line 230
  3. Very specific:

    Always use size="small" for compact display

    line 285
  4. Distinctive:

    Every component MUST follow these rules:

    line 295
  5. Very specific:

    Verify all PrimeVue components used are auto-imported (not in the FormKit-excluded list — see primevue-lookup skill)

    line 333
  6. Very specific:

    Check dark mode: every bg-*, text-*, border-* class has a dark: variant

    line 335
  7. Very specific:

    Verify i18n keys exist in ALL 4 locale files for any t('key') calls

    line 336
  8. Very specific:

    If unsure about PrimeVue component props/events, use mcp__primevue__get_component_props to verify

    line 337

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Scaffold a New Frontend Component

Generate a new Vue component. Component name and type should be provided via $ARGUMENTS.

Before You Start

  1. Read the frontend scope guide: packages/web/CLAUDE.md
  2. 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

Step 1: Determine Component Type

Based on the user's description, identify the component pattern:

PatternWhen to Use
CardClickable item in a grid, shows summary info
Empty CardDashed-border placeholder for "add new" action
ModalDialog for create/edit forms with v-model visibility
List/TableDataTable with columns, selection, and custom templates
DisplayRead-only data visualization (charts, stats, details)
FormFormKit-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:

ElementClasses
Containerrounded-xl border border-surface-200 p-4 dark:border-surface-800
Hoverhover:bg-surface-100 hover:dark:bg-surface-800
Activebg-surface-100 dark:bg-surface-800
Icon containerrounded-full bg-white p-3 dark:bg-surface-900
Titlefont-semibold opacity-80
Subtitletext-xs font-light opacity-70
Descriptiontext-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:

ElementClasses
Containerrounded-xl border-2 border-dashed border-surface-300 p-4 dark:border-surface-600
Hoverhover:border-primary-500 hover:bg-surface-50 dark:hover:bg-surface-800
Iconpi pi-plus-circle text-surface-400 at 1.5rem
Titlefont-medium text-surface-600 dark:text-surface-400
Descriptiontext-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-model pattern: prop modelValue + emit update:modelValue
  • Parent uses <MyModal v-model="isOpen" @success="handleSuccess" />
  • PrimeVue Dialog with modal prop 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 selection
  • data-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 a dark:bg-* variant, every text-* has dark: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 actions
  • useToast() 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 components

Step 4: Verify

  1. Confirm the component auto-import name resolves: {Domain}/{ComponentName}.vue becomes {Domain}{ComponentName}
  2. Verify all PrimeVue components used are auto-imported (not in the FormKit-excluded list — see primevue-lookup skill)
  3. Check dark mode: every bg-*, text-*, border-* class has a dark: variant
  4. Verify i18n keys exist in ALL 4 locale files for any t('key') calls
  5. If unsure about PrimeVue component props/events, use mcp__primevue__get_component_props to 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

ProblemCauseFix
Component not auto-importedWrong directory or namingMust be under components/ in PascalCase directory, file must be PascalCase .vue
Raw HTML elements in templateNot using PrimeVueReplace button with Button, input with FormKit input, select with FormKit Select
Dark mode colors brokenMissing dark: variantsEvery bg-*, text-*, border-* class needs a corresponding dark: variant
Props not type-safeUsing defineProps({}) object syntaxUse generic syntax: defineProps<{ item: ResourceDto }>() with SDK types
FormKit-wrapped component not renderingImported from PrimeVue instead of FormKit24 components excluded from PrimeVue auto-import — see primevue-lookup skill for full list or check nuxt.config.ts
Delete button triggers card clickMissing @click.stopAdd .stop modifier to prevent event bubbling: @click.stop="confirmDelete"

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.