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

scaffold-frontend-page

Skill16 starsChanged 3 days agoApache-2.0View on GitHub

Scaffold a new Nuxt 3 list page with StructuralScreen/Column layout, Pinia-Colada data fetching, card grid, create modal, and NuxtPage outlet for nested detail routes. Use when user says "create a new page", "scaffold a frontend page", "add a list page", "new resource page", "generate a Vue page for X", or "build a page like agents/roles". Do NOT use for detail pages with tabs (use scaffold-frontend-subpage), individual components (use scaffold-frontend-component), or composables only (use scaffold-composable). Generates page + card + i18n following agents.vue and roles.vue patterns.

We split this skill into the rules it gives an agent

17 rules. About 0 are common, 0 are distinctive, and 17 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 17

    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:

    Search packages/web/sdk/client/ for the resource's SDK functions and DTO types.

    line 21
  3. Very specific:

    Look up the actual DTO type name in packages/web/sdk/client/types.gen.ts — types may be named {Resource}Response, Full{Resource}Dto, or …

    line 24
  4. Very specific:

    If composables don't exist yet, create them first using the patterns from /scaffold-composable.

    line 29
  5. Very specific:

    <NuxtPage />: MUST be placed as a sibling of StructuralColumn inside StructuralScreen.

    line 104
  6. Very specific:

    Card grid: Use grid grid-cols-2 gap-4 2xl:grid-cols-2 for consistent card layout.

    line 106
  7. Very specific:

    Navigation: Always use useTenantPath() (wraps useLocalePath() and injects the [tenant] segment).

    line 108
  8. Very specific:

    Create packages/web/components/{Resource}/Card.vue following this exact pattern:

    line 113
  9. Very specific:

    Add translation keys in all 4 locale files (packages/web/i18n/locales/{de,en,fr,it}.yaml):

    line 178
  10. Very specific:

    Check Nuxt file-based routing resolves the new page: filename must be {resource}s.vue (plural) in pages/[tenant]/service/

    line 197
  11. Very specific:

    Verify NuxtPage renders nested routes: create a placeholder pages/[tenant]/service/{resource}s/ directory if detail routes are planned

    line 199
  12. Very specific:

    Check sidebar registration: useSuite() → useApps() fetches service definitions from the API — the new resource may need API-side …

    line 201
  13. Very specific:

    Verify i18n keys exist in ALL 4 locale files (de.yaml, en.yaml, fr.yaml, it.yaml)

    line 203
  14. Very specific:

    If unsure about Nuxt routing conventions, use mcp__nuxt__get-documentation-page with /docs/4.x/guide/directory-structure/pages or …

    line 204
  15. Very specific:

    Tailwind only: No custom CSS (exception: scoped :deep() for PrimeVue overrides)

    line 210
  16. Very specific:

    SDK types for props: Import from @core/sdk/client, never define manually

    line 211
  17. Very specific:

    useTenantPath(): Always for navigation -- router.push(tenantPath('/service/path')) (auto-imported)

    line 214

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

Generate a list page for a new resource. The resource name should be provided via $ARGUMENTS.

Before You Start

  1. Read the frontend scope guide: packages/web/CLAUDE.md
  2. Study these reference pages:
    • Simple list: packages/web/pages/[tenant]/service/roles.vue
    • Complex list with grouping: packages/web/pages/[tenant]/service/agents.vue
    • Knowledge list: packages/web/pages/[tenant]/service/knowledge.vue

Step 1: Check SDK Availability

Search packages/web/sdk/client/ for the resource's SDK functions and DTO types. If they don't exist, warn the user to run /generate-sdk first.

Look up the actual DTO type name in packages/web/sdk/client/types.gen.ts — types may be named {Resource}Response, Full{Resource}Dto, or {Resource}Dto depending on the API schema.

Step 2: Create Composables

If composables don't exist yet, create them first using the patterns from /scaffold-composable. At minimum you need:

  • composables/{resource}/use{Resource}s.ts — List query
  • composables/{resource}/useCreate{Resource}.ts — Create mutation (if applicable)

Step 3: Create the List Page

Create packages/web/pages/[tenant]/service/{resource}s.vue:

<template>
  <StructuralScreen>
    <StructuralColumn
      :title="t('<resource>.title')"
      :loading="<resource>sAreLoading"
    >
      <div class="flex flex-col gap-2">
        <!-- Create button (top right) -->
        <div class="flex w-full justify-end">
          <Button
            :label="t('<resource>.create_new')"
            icon="pi pi-plus"
            @click="createModalOpen = true"
          />
        </div>

        <!-- Card grid -->
        <div class="grid grid-cols-2 gap-4 2xl:grid-cols-2">
          <<Resource>Card
            v-for="item in <resource>s"
            :key="item.id"
            :<resource>="item"
            @click="() => toDetail(item)"
          />
        </div>
      </div>

      <!-- Create modal -->
      <Dialog
        v-model:visible="createModalOpen"
        modal
        :header="t('<resource>.create_new')"
      >
        <<Resource>Create @close="createModalOpen = false" />
      </Dialog>
    </StructuralColumn>

    <!-- Nested detail page outlet -->
    <NuxtPage />
  </StructuralScreen>
</template>

<script setup lang="ts">
import type { <Resource>Dto } from '@core/sdk/client'

const router = useRouter()
const tenantPath = useTenantPath()
const { t } = useI18n()

const { <resource>s, <resource>sAreLoading } = use<Resource>s()

const createModalOpen = ref(false)

const toDetail = (item: <Resource>Dto) => {
  router.push(tenantPath(`/service/<resource>s/${item.id}/overview`))
}
</script>

Key Structural Elements

  1. <StructuralScreen>: Top-level wrapper. Provides h-[calc(100vh-50px)] viewport, bg-surface-50 dark:bg-surface-950, horizontal flex at 2xl:.
  2. <StructuralColumn>: Card container with rounded-3xl, loading progress bar, title/close button. Props: title, loading, size, closeRoute, childColumn.
  3. <NuxtPage />: MUST be placed as a sibling of StructuralColumn inside StructuralScreen. This renders nested detail routes as a second column.
  4. Card grid: Use grid grid-cols-2 gap-4 2xl:grid-cols-2 for consistent card layout.
  5. Create button: Top-right aligned with flex w-full justify-end.
  6. Navigation: Always use useTenantPath() (wraps useLocalePath() and injects the [tenant] segment); paths are written without locale or tenant (/service/roles).

Step 4: Create Card Component

Create packages/web/components/{Resource}/Card.vue following this exact pattern:

<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', <resource>)"
  >
    <!-- Header: Icon + Title + Status -->
    <div class="flex items-center justify-between gap-4">
      <div class="flex items-center justify-start gap-2">
        <div class="flex items-center justify-center rounded-full bg-white p-3 dark:bg-surface-900">
          <Icon :name="icon" size="1.5em" />
        </div>
        <div>
          <h3 class="font-semibold opacity-80">{{ <resource>.name }}</h3>
          <p class="text-xs font-light opacity-70">{{ <resource>.id }}</p>
        </div>
      </div>
      <div class="flex items-center gap-2">
        <Tag :value="statusLabel" :severity="statusSeverity" />
      </div>
    </div>

    <!-- Description -->
    <div>
      <span class="text-xs">{{ <resource>.description }}</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { <Resource>Dto } from '@core/sdk/client'

const props = defineProps<{
  <resource>: <Resource>Dto
}>()

const emit = defineEmits<{
  click: [<resource>: <Resource>Dto]
}>()

const route = useRoute()
const { t } = useI18n()

const isActive = computed(() => {
  return route.params.<resource>_id === props.<resource>.id
})
</script>

Card Design Rules

  • Container: rounded-xl border border-surface-200 p-4 with hover and dark mode variants
  • Active state: bg-surface-100 dark:bg-surface-800 when route matches
  • Icon container: rounded-full bg-white p-3 dark:bg-surface-900 with Icon size="1.5em"
  • Title: font-semibold opacity-80
  • Subtitle: text-xs font-light opacity-70
  • Description: text-xs
  • Status: PrimeVue Tag with severity (success, danger, secondary)
  • Delete button: Button icon="pi pi-trash" severity="secondary" text rounded size="small" with @click.stop

Step 5: Add i18n Keys

Add translation keys in all 4 locale files (packages/web/i18n/locales/{de,en,fr,it}.yaml):

{resource}:
  title: "{Resource}s"
  create_new: "Create {Resource}"
  list:
    empty: "No {resource}s found"
  delete:
    title: "Delete {Resource}"
    confirmMessage: "Are you sure you want to delete this {resource}?"
    button: "Delete"
    success: "{Resource} deleted successfully"
    error: "Failed to delete {resource}"
    cancel: "Cancel"

Step 6: Verify

  1. Check Nuxt file-based routing resolves the new page: filename must be {resource}s.vue (plural) in pages/[tenant]/service/
  2. Verify NuxtPage renders nested routes: create a placeholder pages/[tenant]/service/{resource}s/ directory if detail routes are planned
  3. Check sidebar registration: useSuite() → useApps() fetches service definitions from the API — the new resource may need API-side registration
  4. Verify i18n keys exist in ALL 4 locale files (de.yaml, en.yaml, fr.yaml, it.yaml)
  5. If unsure about Nuxt routing conventions, use mcp__nuxt__get-documentation-page with /docs/4.x/guide/directory-structure/pages or mcp__context7__query-docs with library /websites/nuxt

Key Conventions

  • PrimeVue components only: Button, Tag, Dialog, DataTable -- never raw HTML for interactive elements
  • Tailwind only: No custom CSS (exception: scoped :deep() for PrimeVue overrides)
  • SDK types for props: Import from @core/sdk/client, never define manually
  • i18n all text: {{ t('key.path') }} for everything user-visible
  • Pinia-Colada: defineQuery/defineMutation -- never raw fetch or global stores
  • useTenantPath(): Always for navigation -- router.push(tenantPath('/service/path')) (auto-imported)
  • useConfirm() + useToast(): For delete confirmations and success/error messages

Examples

Input: $ARGUMENTS = "pipeline"

Output files created:

  1. packages/web/pages/[tenant]/service/pipelines.vue -- List page with card grid
  2. packages/web/components/Pipeline/Card.vue -- Resource card component
  3. packages/web/composables/pipeline/usePipelines.ts -- List query composable
  4. packages/web/composables/pipeline/useCreatePipeline.ts -- Create mutation composable
  5. i18n keys added to all 4 locale files (de.yaml, en.yaml, fr.yaml, it.yaml)

Input: $ARGUMENTS = "connector"

Output: Same structure with connector replacing pipeline -- connectors.vue, Connector/Card.vue, useConnectors.ts, etc.

Troubleshooting

ProblemCauseFix
SDK types not found for resourceSDK not generated yetTell user to run /generate-sdk first
Page not appearing in sidebarNot registered in service configCheck useSuite() / useApps() API service definitions
Cards not renderingComposable returns empty arrayVerify SDK endpoint URL and check browser Network tab for API errors
i18n keys showing raw pathsMissing translation keysEnsure keys were added to ALL 4 locale files (de, en, fr, it)
Route not matching / 404File naming mismatchVerify filename matches Nuxt file-based routing: {resource}s.vue (plural)
Dark mode broken on cardsMissing dark: variantEvery bg-* class must have a corresponding dark:bg-* class

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.