@elyracode/stack-vilt 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @elyracode/stack-vilt
2
+
3
+ Elyra stack profile for **VILT** (Vue 3, Inertia.js, Laravel, Tailwind CSS).
4
+
5
+ ## Install
6
+
7
+ ```
8
+ elyra install npm:@elyracode/stack-vilt
9
+ ```
10
+
11
+ ## What's included
12
+
13
+ - **Skills**: Deep VILT stack knowledge (Inertia.js protocol, Vue 3 Composition API, Laravel controller patterns, useForm, TypeScript type sync, partial reloads, file structure conventions, common gotchas)
14
+ - **Commands**: `/vilt:info` -- show stack profile status
15
+
16
+ ## Stack Detection
17
+
18
+ Elyra automatically detects VILT stack projects and suggests this package.
@@ -0,0 +1,10 @@
1
+ import type { ExtensionAPI } from "@elyracode/coding-agent";
2
+
3
+ export default function (elyra: ExtensionAPI) {
4
+ elyra.registerCommand("vilt:info", {
5
+ description: "Show detected VILT stack information",
6
+ handler: async (ctx) => {
7
+ ctx.ui.notify("info", "VILT stack profile loaded. Skills: vilt-stack. Use /skill:vilt-stack for full reference.");
8
+ },
9
+ });
10
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@elyracode/stack-vilt",
3
+ "version": "0.3.0",
4
+ "description": "Elyra stack profile for VILT (Vue 3, Inertia.js, Laravel, Tailwind CSS)",
5
+ "type": "module",
6
+ "keywords": ["elyra-package", "vilt", "vue", "inertia", "laravel", "tailwind"],
7
+ "license": "MIT",
8
+ "author": "Knut W. Horne",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kwhorne/elyra.git",
12
+ "directory": "packages/stack-vilt"
13
+ },
14
+ "elyra": {
15
+ "skills": ["./skills"],
16
+ "extensions": ["./extensions/index.ts"]
17
+ },
18
+ "peerDependencies": {
19
+ "@elyracode/coding-agent": "*",
20
+ "typebox": "*"
21
+ },
22
+ "scripts": {
23
+ "clean": "echo 'nothing to clean'",
24
+ "build": "echo 'nothing to build'",
25
+ "check": "echo 'nothing to check'"
26
+ }
27
+ }
@@ -0,0 +1,326 @@
1
+ ---
2
+ name: vilt-stack
3
+ description: Deep knowledge about the VILT stack - Vue 3, Inertia.js, Laravel, and Tailwind CSS. Use when working on VILT stack projects, Inertia pages, Vue components, Laravel controllers with Inertia responses, or TypeScript type sync between backend and frontend.
4
+ ---
5
+
6
+ # VILT Stack Reference
7
+
8
+ ## Architecture
9
+
10
+ The VILT stack connects:
11
+ - **Laravel** as the backend (routing, controllers, Eloquent, middleware, validation)
12
+ - **Inertia.js** as the glue layer (replaces traditional API + SPA routing)
13
+ - **Vue 3** with `<script setup>` and Composition API for the frontend
14
+ - **Tailwind CSS** for utility-first styling
15
+
16
+ ### How Inertia Works
17
+
18
+ Inertia is NOT an API. It's a protocol:
19
+ 1. First request: Server returns a full HTML page with the Vue app + initial page data as JSON
20
+ 2. Subsequent navigation: Inertia intercepts links, makes XHR requests, server returns only JSON props
21
+ 3. Vue swaps the page component without a full page reload
22
+
23
+ The server always controls routing. There is no Vue Router.
24
+
25
+ ## Laravel Side
26
+
27
+ ### Controller Pattern
28
+ ```php
29
+ use Inertia\Inertia;
30
+ use Inertia\Response;
31
+
32
+ class UserController extends Controller
33
+ {
34
+ public function index(): Response
35
+ {
36
+ return Inertia::render('Users/Index', [
37
+ 'users' => User::query()
38
+ ->select('id', 'name', 'email')
39
+ ->paginate(10),
40
+ 'filters' => request()->only(['search', 'role']),
41
+ ]);
42
+ }
43
+
44
+ public function create(): Response
45
+ {
46
+ return Inertia::render('Users/Create');
47
+ }
48
+
49
+ public function store(StoreUserRequest $request)
50
+ {
51
+ User::create($request->validated());
52
+ return redirect()->route('users.index')
53
+ ->with('success', 'User created.');
54
+ }
55
+
56
+ public function edit(User $user): Response
57
+ {
58
+ return Inertia::render('Users/Edit', [
59
+ 'user' => $user->only('id', 'name', 'email', 'role'),
60
+ ]);
61
+ }
62
+
63
+ public function update(UpdateUserRequest $request, User $user)
64
+ {
65
+ $user->update($request->validated());
66
+ return redirect()->route('users.index')
67
+ ->with('success', 'User updated.');
68
+ }
69
+
70
+ public function destroy(User $user)
71
+ {
72
+ $user->delete();
73
+ return redirect()->route('users.index')
74
+ ->with('success', 'User deleted.');
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Shared Data (available on every page)
80
+ ```php
81
+ // app/Http/Middleware/HandleInertiaRequests.php
82
+ public function share(Request $request): array
83
+ {
84
+ return [
85
+ ...parent::share($request),
86
+ 'auth' => [
87
+ 'user' => $request->user()?->only('id', 'name', 'email', 'role'),
88
+ ],
89
+ 'flash' => [
90
+ 'success' => fn () => $request->session()->get('success'),
91
+ 'error' => fn () => $request->session()->get('error'),
92
+ ],
93
+ ];
94
+ }
95
+ ```
96
+
97
+ ### Lazy Props (only loaded when needed)
98
+ ```php
99
+ return Inertia::render('Users/Show', [
100
+ 'user' => $user,
101
+ 'activity' => Inertia::lazy(fn () => $user->activity()->latest()->get()),
102
+ ]);
103
+ ```
104
+
105
+ ### Partial Reloads
106
+ ```php
107
+ // Only reload specific props
108
+ return Inertia::render('Dashboard', [
109
+ 'stats' => fn () => Stats::calculate(),
110
+ 'notifications' => fn () => auth()->user()->unreadNotifications,
111
+ ]);
112
+ ```
113
+
114
+ ## Vue 3 Side
115
+
116
+ ### Page Component Pattern
117
+ ```vue
118
+ <script setup lang="ts">
119
+ import { Head, Link, useForm, router } from '@inertiajs/vue3'
120
+ import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
121
+
122
+ interface User {
123
+ id: number
124
+ name: string
125
+ email: string
126
+ role: string
127
+ }
128
+
129
+ interface Props {
130
+ users: {
131
+ data: User[]
132
+ links: Array<{ url: string | null; label: string; active: boolean }>
133
+ current_page: number
134
+ last_page: number
135
+ }
136
+ filters: {
137
+ search?: string
138
+ role?: string
139
+ }
140
+ }
141
+
142
+ const props = defineProps<Props>()
143
+
144
+ function destroy(id: number) {
145
+ if (confirm('Are you sure?')) {
146
+ router.delete(route('users.destroy', id))
147
+ }
148
+ }
149
+ </script>
150
+
151
+ <template>
152
+ <Head title="Users" />
153
+ <AuthenticatedLayout>
154
+ <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
155
+ <div class="flex justify-between items-center mb-6">
156
+ <h1 class="text-2xl font-semibold text-gray-900 dark:text-white">Users</h1>
157
+ <Link :href="route('users.create')" class="btn btn-primary">
158
+ Add User
159
+ </Link>
160
+ </div>
161
+
162
+ <div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
163
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
164
+ <thead class="bg-gray-50 dark:bg-gray-900">
165
+ <tr>
166
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
167
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Email</th>
168
+ <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
169
+ </tr>
170
+ </thead>
171
+ <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
172
+ <tr v-for="user in users.data" :key="user.id">
173
+ <td class="px-6 py-4 whitespace-nowrap">{{ user.name }}</td>
174
+ <td class="px-6 py-4 whitespace-nowrap">{{ user.email }}</td>
175
+ <td class="px-6 py-4 whitespace-nowrap text-right">
176
+ <Link :href="route('users.edit', user.id)" class="text-blue-600 hover:text-blue-900">Edit</Link>
177
+ <button @click="destroy(user.id)" class="ml-4 text-red-600 hover:text-red-900">Delete</button>
178
+ </td>
179
+ </tr>
180
+ </tbody>
181
+ </table>
182
+ </div>
183
+ </div>
184
+ </AuthenticatedLayout>
185
+ </template>
186
+ ```
187
+
188
+ ### Form Handling with useForm
189
+ ```vue
190
+ <script setup lang="ts">
191
+ import { useForm } from '@inertiajs/vue3'
192
+
193
+ const form = useForm({
194
+ name: '',
195
+ email: '',
196
+ role: 'user',
197
+ })
198
+
199
+ function submit() {
200
+ form.post(route('users.store'), {
201
+ onSuccess: () => form.reset(),
202
+ })
203
+ }
204
+ </script>
205
+
206
+ <template>
207
+ <form @submit.prevent="submit" class="space-y-4">
208
+ <div>
209
+ <label class="block text-sm font-medium text-gray-700">Name</label>
210
+ <input v-model="form.name" type="text" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
211
+ <p v-if="form.errors.name" class="mt-1 text-sm text-red-600">{{ form.errors.name }}</p>
212
+ </div>
213
+ <div>
214
+ <label class="block text-sm font-medium text-gray-700">Email</label>
215
+ <input v-model="form.email" type="email" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" />
216
+ <p v-if="form.errors.email" class="mt-1 text-sm text-red-600">{{ form.errors.email }}</p>
217
+ </div>
218
+ <button type="submit" :disabled="form.processing" class="btn btn-primary">
219
+ {{ form.processing ? 'Saving...' : 'Save' }}
220
+ </button>
221
+ </form>
222
+ </template>
223
+ ```
224
+
225
+ ### TypeScript Type Definitions
226
+ ```typescript
227
+ // resources/js/types/index.d.ts
228
+ export interface User {
229
+ id: number
230
+ name: string
231
+ email: string
232
+ email_verified_at?: string
233
+ role: 'admin' | 'user'
234
+ }
235
+
236
+ export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
237
+ auth: {
238
+ user: User
239
+ }
240
+ flash: {
241
+ success?: string
242
+ error?: string
243
+ }
244
+ }
245
+ ```
246
+
247
+ ### Inertia Router Methods
248
+ ```typescript
249
+ import { router } from '@inertiajs/vue3'
250
+
251
+ // Navigate
252
+ router.visit('/users')
253
+ router.get('/users')
254
+ router.post('/users', data)
255
+ router.put('/users/1', data)
256
+ router.patch('/users/1', data)
257
+ router.delete('/users/1')
258
+
259
+ // With options
260
+ router.post('/users', data, {
261
+ preserveScroll: true,
262
+ preserveState: true,
263
+ only: ['users'], // Partial reload
264
+ onSuccess: () => {},
265
+ onError: (errors) => {},
266
+ onFinish: () => {},
267
+ })
268
+
269
+ // Reload current page props
270
+ router.reload({ only: ['notifications'] })
271
+ ```
272
+
273
+ ## File Structure Conventions
274
+
275
+ ```
276
+ app/
277
+ Http/
278
+ Controllers/ # Controllers return Inertia::render()
279
+ Middleware/
280
+ HandleInertiaRequests.php # Shared data
281
+ Requests/ # Form validation
282
+ Models/
283
+ resources/
284
+ js/
285
+ Pages/ # Inertia page components (maps to Inertia::render paths)
286
+ Users/
287
+ Index.vue
288
+ Create.vue
289
+ Edit.vue
290
+ Dashboard.vue
291
+ Components/ # Reusable Vue components
292
+ Layouts/ # Layout components
293
+ AuthenticatedLayout.vue
294
+ GuestLayout.vue
295
+ Composables/ # Vue composables (use* functions)
296
+ types/ # TypeScript type definitions
297
+ index.d.ts
298
+ app.ts # Vue app bootstrap + Inertia setup
299
+ routes/
300
+ web.php # All routes (Inertia handles them server-side)
301
+ ```
302
+
303
+ ## Type Sync Pattern (Backend to Frontend)
304
+
305
+ When changing data sent from Laravel to Vue, update both sides:
306
+
307
+ 1. **Laravel controller** -- change what's passed to `Inertia::render()`
308
+ 2. **TypeScript types** -- update the interface in `resources/js/types/`
309
+ 3. **Vue component** -- update `defineProps<Props>()` to match
310
+
311
+ Example: Adding a `phone` field to User:
312
+ 1. Migration: `$table->string('phone')->nullable()`
313
+ 2. Controller: add `'phone'` to the select/only list
314
+ 3. TypeScript: add `phone?: string` to `User` interface
315
+ 4. Vue: add the input field bound to the prop
316
+
317
+ ## Common Gotchas
318
+
319
+ 1. **No Vue Router**: Inertia replaces client-side routing. Use `<Link>` and `router` from `@inertiajs/vue3`, never `vue-router`.
320
+ 2. **Props are reactive, not the page**: When Inertia navigates, it replaces the entire page component. State in `ref()`/`reactive()` resets. Use `preserveState: true` or Pinia for persistent state.
321
+ 3. **Validation errors**: Laravel validation errors are automatically available via `form.errors` or `usePage().props.errors`. No manual error handling needed.
322
+ 4. **Flash messages**: Use Laravel's `->with('success', 'msg')` on redirects. Access via shared data in `HandleInertiaRequests.php`.
323
+ 5. **File uploads**: Use `form.post()` with FormData. Inertia handles multipart automatically when the form has File values.
324
+ 6. **SSR**: Inertia supports SSR with `@inertiajs/vue3/server`. Configure in `vite.config.ts` with the `laravel-vite-plugin`.
325
+ 7. **Ziggy routes**: Use `route('name')` in Vue via the Ziggy package. Never hardcode URLs.
326
+ 8. **Partial reloads**: Wrap expensive props in closures `fn () =>` in the controller. Request them with `router.reload({ only: ['prop'] })`.