agentleFS
zhd4nzhd4n/skills/shadcn-vue-laravel/SKILL.md

shadcn-vue-laravel

Skill0 starsChanged 7 months agoMITView on GitHub

Use when adapting shadcn-vue components or examples to a Laravel + Inertia + Vue application, especially when examples assume Nuxt, Rails, src-based paths, or vee-validate-first forms.

The file

Show its rules and text

We split this skill into the rules it gives an agent

16 rules. About 0 are common, 3 are distinctive, and 13 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 13

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use this skill as the translation layer from generic shadcn-vue examples to a Laravel + Inertia + Vue 3 codebase.

    line 8
  2. Distinctive:

    Start from a working Laravel + Inertia + Vue application.

    line 23
  3. Very specific:

    Keep generated components under resources/js/components/ui unless the project already uses a different component root.

    line 25
  4. Very specific:

    Ensure @/* resolves to ./resources/js/* in tsconfig.json.

    line 26
  5. Very specific:

    If the project uses @ imports and vite.config.ts does not already define the alias, add one that points to resources/js.

    line 27
  6. Very specific:

    Keep Inertia boot files ESM-only.

    line 29
  7. Distinctive:

    If you need the Vite alias, use:

    line 31
  8. Very specific:

    If the app adopts @inertiajs/vite in v3, let that plugin own page resolution and dev SSR plumbing instead of copying older manual bootstrap …

    line 46
  9. Very specific:

    Use Inertia <Form> for straightforward server-driven forms where inputs can submit through name attributes.

    line 50
  10. Very specific:

    Use useForm when the component owns reactive state, conditional fields, client-side transforms, file uploads, or progressive validation.

    line 51
  11. Very specific:

    Do not use shadcn-vue FormField, FormItem, FormLabel, or FormMessage inside an Inertia <Form> unless you intentionally wire vee-validate …

    line 54
  12. Distinctive:

    Use plain shadcn-vue inputs plus manual error rendering:

    line 59
  13. Very specific:

    Use <Link> instead of raw <a> for internal navigation that should preserve Inertia behavior.

    line 177
  14. Very specific:

    If a component pattern depends on global router listeners or visit callbacks, verify the exact event names in $laravel-inertia before …

    line 182
  15. Very specific:

    Read references/flash-toast.md when implementing centralized Sonner toasts or a reusable flash composable.

    line 188
  16. Very specific:

    Use a small useAppearance composable and toggle the .dark class on <html>.

    line 204

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 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

shadcn-vue for Laravel Inertia

Use this skill as the translation layer from generic shadcn-vue examples to a Laravel + Inertia + Vue 3 codebase. Optimize for Laravel starter-kit conventions: Blade root view, @inertiajs/vue3, TypeScript, Tailwind, and frontend code under resources/js.

For broader Inertia application behavior such as useHttp, layout props, optimistic updates, deferred props, prefetch policy, router events, and Laravel adapter testing, use the companion skill $laravel-inertia.

Quick Translation Rules

  • Nuxt data APIs such as useFetch and useAsyncData: replace with controller props, deferred props, partial reloads, or explicit Inertia visits.
  • useRouter() from Nuxt or Vue Router: replace with router from @inertiajs/vue3.
  • <NuxtLink> or <RouterLink>: replace with <Link> from @inertiajs/vue3.
  • useHead(): replace with <Head> from @inertiajs/vue3.
  • src/..., app/frontend/..., or Rails frontend paths: translate to the existing Laravel structure, usually resources/js/....
  • shadcn-vue form examples built around vee-validate: do not copy them blindly into Inertia <Form> flows.

Setup

  1. Start from a working Laravel + Inertia + Vue application.
  2. Run npx shadcn-vue@latest init.
  3. Keep generated components under resources/js/components/ui unless the project already uses a different component root.
  4. Ensure @/* resolves to ./resources/js/* in tsconfig.json.
  5. If the project uses @ imports and vite.config.ts does not already define the alias, add one that points to resources/js.
  6. Preserve the page and layout casing already used by the project. Do not rename Pages to pages, or the reverse, just to match an example.
  7. Keep Inertia boot files ESM-only. Do not introduce require() into app.ts, SSR entrypoints, or Vite config.

If you need the Vite alias, use:

import path from 'node:path'
import { defineConfig } from 'vite'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'resources/js'),
    },
  },
})

If the app adopts @inertiajs/vite in v3, let that plugin own page resolution and dev SSR plumbing instead of copying older manual bootstrap patterns back into the project.

Choose <Form> vs useForm

  • Use Inertia <Form> for straightforward server-driven forms where inputs can submit through name attributes.
  • Use useForm when the component owns reactive state, conditional fields, client-side transforms, file uploads, or progressive validation.
  • v-model is correct with useForm.
  • v-model alone is not the source of truth for an Inertia <Form>; the submitted payload comes from input name attributes.
  • Do not use shadcn-vue FormField, FormItem, FormLabel, or FormMessage inside an Inertia <Form> unless you intentionally wire vee-validate for that page. They are not part of the default Laravel + Inertia path.
  • In v3, useForm().processing remains true until onFinish, so disable buttons and pending UI from processing instead of resetting eagerly.

Default <Form> Pattern

Use plain shadcn-vue inputs plus manual error rendering:

<script setup lang="ts">
import { Form } from '@inertiajs/vue3'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
</script>

<template>
  <Form method="post" action="/users">
    <template #default="{ errors, processing }">
      <div class="space-y-4">
        <div>
          <Label for="name">Name</Label>
          <Input id="name" name="name" />
          <p v-if="errors.name" class="text-sm text-destructive">{{ errors.name }}</p>
        </div>

        <div>
          <Label for="email">Email</Label>
          <Input id="email" name="email" type="email" />
          <p v-if="errors.email" class="text-sm text-destructive">{{ errors.email }}</p>
        </div>

        <Button type="submit" :disabled="processing">
          {{ processing ? 'Creating...' : 'Create User' }}
        </Button>
      </div>
    </template>
  </Form>
</template>

Inside <Form>, Select must carry a name prop:

<template>
  <Select name="role" default-value="member">
    <SelectTrigger><SelectValue placeholder="Select role" /></SelectTrigger>
    <SelectContent>
      <SelectItem value="admin">Admin</SelectItem>
      <SelectItem value="member">Member</SelectItem>
    </SelectContent>
  </Select>
</template>

Default useForm Pattern

Switch to useForm when local reactive state is the point:

<script setup lang="ts">
import { useForm } from '@inertiajs/vue3'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'

const form = useForm({
  name: '',
  email: '',
  role: 'member',
})

const submit = () => form.post('/users')
</script>

<template>
  <form class="space-y-4" @submit.prevent="submit">
    <div>
      <Label for="name">Name</Label>
      <Input id="name" v-model="form.name" />
      <p v-if="form.errors.name" class="text-sm text-destructive">{{ form.errors.name }}</p>
    </div>

    <div>
      <Label for="email">Email</Label>
      <Input id="email" v-model="form.email" type="email" />
      <p v-if="form.errors.email" class="text-sm text-destructive">{{ form.errors.email }}</p>
    </div>

    <Button type="submit" :disabled="form.processing">
      {{ form.processing ? 'Creating...' : 'Create User' }}
    </Button>
  </form>
</template>

Shadcn-Relevant v3 Helper

useFormContext() lets deep child components read the parent <Form> state. Use it for reusable shadcn wrappers, sticky action bars, field summaries, or nested submit controls without prop drilling.

For broader Inertia primitives such as Precognition, cache invalidation, router error events, deferred props, and prefetch strategy, load $laravel-inertia instead of expanding this skill.

Example child component using useFormContext():

<script setup lang="ts">
import { useFormContext } from '@inertiajs/vue3'
import { Button } from '@/components/ui/button'

const form = useFormContext()
</script>

<template>
  <div v-if="form" class="flex items-center gap-2">
    <span v-if="form.isDirty" class="text-sm text-muted-foreground">Unsaved changes</span>
    <Button type="button" :disabled="form.processing" @click="form.submit()">
      Save
    </Button>
  </div>
</template>

Component Gotchas

  • Use <Link> instead of raw <a> for internal navigation that should preserve Inertia behavior.
  • For table sorting, filtering, tabs, and dialogs driven by URL state, use router.get() or router.reload() with preserveState and preserveScroll when appropriate.
  • usePage() is reactive; wrap derived values in computed(() => page.props...) instead of destructuring once.
  • shadcn-vue Dialog emits update:open, not close.
  • Translate Rails layout snippets to resources/views/app.blade.php.
  • If a component pattern depends on global router listeners or visit callbacks, verify the exact event names in $laravel-inertia before copying older snippets.

Flash Toasts

Prefer the Inertia v3 flash-data flow: server-side Inertia::flash(...), then page.flash, per-visit onFlash, or router.on('flash', ...) on the client. Use router.flash(...) for client-only ephemeral UI state when no server round trip is needed. Treat props.flash through HandleInertiaRequests as a compatibility path for older code, not the default for new work.

Read references/flash-toast.md when implementing centralized Sonner toasts or a reusable flash composable. Do not load it for pages that only render one flash value inline.

Dark Mode

npx shadcn-vue@latest init gives you the CSS variables. The Laravel-specific part is preventing FOUC in resources/views/app.blade.php before @vite(...) runs:

<script>
  document.documentElement.classList.toggle(
    'dark',
    localStorage.appearance === 'dark' ||
      (!('appearance' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches),
  );
</script>

Use a small useAppearance composable and toggle the .dark class on <html>. Do not pull in Nuxt color-mode or Rails-specific theme helpers.

Troubleshooting

SymptomCauseFix
FormField or FormMessage crashshadcn-vue form components were copied without vee-validate contextUse plain Input / Label plus Inertia error rendering, or intentionally wire vee-validate
Select value is missing on submit<Select> inside <Form> has no name propAdd name="field"
v-model value never reaches the server<Form> is reading DOM field names, not your local refsUse name attributes or switch to useForm
Shared props look stale after navigationA usePage() value was destructured onceWrap derived values in computed()
Dialog close handler never fires@close was used instead of @update:openHandle @update:open="(open) => { if (!open) close() }"
Global error listener never runsv2 event names were copied into a v3 appReplace invalid with httpException and exception with networkError
Dark mode flashes on first paintTheme script runs after Vite assetsPut the script in resources/views/app.blade.php before @vite(...)

References

  • Load references/components.md when you need extended patterns such as AlertDialog, Sheet, Tabs, DropdownMenu, Pagination, Search, Date Picker, or Breadcrumbs.
  • Load references/flash-toast.md when you need centralized flash-toast wiring with Sonner.

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.