@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 +18 -0
- package/extensions/index.ts +10 -0
- package/package.json +27 -0
- package/skills/vilt-stack/SKILL.md +326 -0
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'] })`.
|