@elyracode/stack-rilt 0.4.2
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 +27 -0
- package/extensions/index.ts +10 -0
- package/package.json +27 -0
- package/skills/rilt-stack/SKILL.md +481 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @elyracode/stack-rilt
|
|
2
|
+
|
|
3
|
+
Elyra stack profile for **RILT** (React 19, Inertia.js 2, Laravel, Tailwind CSS 4) with shadcn/ui.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
elyra install npm:@elyracode/stack-rilt
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What's included
|
|
12
|
+
|
|
13
|
+
- **Skills**: Deep RILT stack knowledge (Inertia.js 2 protocol, React 19 with TypeScript, useForm, shadcn/ui components, Laravel controller patterns, TypeScript type sync, layout variants, common gotchas)
|
|
14
|
+
- **Commands**: `/rilt:info` -- show stack profile status
|
|
15
|
+
|
|
16
|
+
## Stack Detection
|
|
17
|
+
|
|
18
|
+
Elyra automatically detects RILT stack projects (React + Inertia + Laravel + Tailwind in your dependencies).
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
> Build a products CRUD with shadcn/ui table and dialog
|
|
24
|
+
> Create a dashboard with stats cards using shadcn components
|
|
25
|
+
> Set up form validation with useForm and shadcn inputs
|
|
26
|
+
> Add a confirmation dialog before deleting a record
|
|
27
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@elyracode/coding-agent";
|
|
2
|
+
|
|
3
|
+
export default function (elyra: ExtensionAPI): void {
|
|
4
|
+
elyra.registerCommand("rilt:info", {
|
|
5
|
+
description: "Show detected RILT stack information",
|
|
6
|
+
handler: async (_args, ctx) => {
|
|
7
|
+
ctx.ui.notify("RILT stack profile loaded. Skills: rilt-stack. Use /skill:rilt-stack for full reference.");
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elyracode/stack-rilt",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Elyra stack profile for RILT (React 19, Inertia.js 2, Laravel, Tailwind CSS 4) with shadcn/ui",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["elyra-package", "rilt", "react", "inertia", "laravel", "tailwind", "shadcn"],
|
|
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-rilt"
|
|
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,481 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rilt-stack
|
|
3
|
+
description: Deep knowledge about the RILT stack - React 19, Inertia.js 2, Laravel, and Tailwind CSS 4 with shadcn/ui. Use when working on RILT stack projects, Inertia pages with React, Laravel controllers with Inertia responses, TypeScript type sync, or shadcn/ui components.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RILT Stack Reference
|
|
7
|
+
|
|
8
|
+
## Architecture
|
|
9
|
+
|
|
10
|
+
The RILT stack connects:
|
|
11
|
+
- **Laravel** as the backend (routing, controllers, Eloquent, middleware, validation, Fortify auth)
|
|
12
|
+
- **Inertia.js 2** as the glue layer (replaces traditional API + SPA routing)
|
|
13
|
+
- **React 19** with TypeScript for the frontend
|
|
14
|
+
- **Tailwind CSS 4** for utility-first styling
|
|
15
|
+
- **shadcn/ui** for pre-built, accessible, customizable components
|
|
16
|
+
|
|
17
|
+
### How Inertia Works
|
|
18
|
+
|
|
19
|
+
Inertia is NOT an API. It's a protocol:
|
|
20
|
+
1. First request: server returns full HTML with React app + initial page data as JSON
|
|
21
|
+
2. Subsequent navigation: Inertia intercepts links, makes XHR, server returns only JSON props
|
|
22
|
+
3. React swaps the page component without full reload
|
|
23
|
+
|
|
24
|
+
The server always controls routing. There is no React Router.
|
|
25
|
+
|
|
26
|
+
## Laravel Side
|
|
27
|
+
|
|
28
|
+
### Controller Pattern
|
|
29
|
+
```php
|
|
30
|
+
use Inertia\Inertia;
|
|
31
|
+
use Inertia\Response;
|
|
32
|
+
|
|
33
|
+
class ProductController extends Controller
|
|
34
|
+
{
|
|
35
|
+
public function index(): Response
|
|
36
|
+
{
|
|
37
|
+
return Inertia::render('Products/Index', [
|
|
38
|
+
'products' => Product::query()
|
|
39
|
+
->select('id', 'name', 'price', 'status')
|
|
40
|
+
->paginate(10),
|
|
41
|
+
'filters' => request()->only(['search', 'status']),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public function create(): Response
|
|
46
|
+
{
|
|
47
|
+
return Inertia::render('Products/Create', [
|
|
48
|
+
'categories' => Category::pluck('name', 'id'),
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public function store(StoreProductRequest $request)
|
|
53
|
+
{
|
|
54
|
+
Product::create($request->validated());
|
|
55
|
+
return redirect()->route('products.index')
|
|
56
|
+
->with('success', 'Product created.');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public function edit(Product $product): Response
|
|
60
|
+
{
|
|
61
|
+
return Inertia::render('Products/Edit', [
|
|
62
|
+
'product' => $product->only('id', 'name', 'price', 'description', 'category_id'),
|
|
63
|
+
'categories' => Category::pluck('name', 'id'),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public function update(UpdateProductRequest $request, Product $product)
|
|
68
|
+
{
|
|
69
|
+
$product->update($request->validated());
|
|
70
|
+
return redirect()->route('products.index')
|
|
71
|
+
->with('success', 'Product updated.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public function destroy(Product $product)
|
|
75
|
+
{
|
|
76
|
+
$product->delete();
|
|
77
|
+
return redirect()->route('products.index')
|
|
78
|
+
->with('success', 'Product deleted.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Shared Data
|
|
84
|
+
```php
|
|
85
|
+
// app/Http/Middleware/HandleInertiaRequests.php
|
|
86
|
+
public function share(Request $request): array
|
|
87
|
+
{
|
|
88
|
+
return [
|
|
89
|
+
...parent::share($request),
|
|
90
|
+
'auth' => [
|
|
91
|
+
'user' => $request->user()?->only('id', 'name', 'email', 'role'),
|
|
92
|
+
],
|
|
93
|
+
'flash' => [
|
|
94
|
+
'success' => fn () => $request->session()->get('success'),
|
|
95
|
+
'error' => fn () => $request->session()->get('error'),
|
|
96
|
+
],
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Lazy Props and Partial Reloads
|
|
102
|
+
```php
|
|
103
|
+
return Inertia::render('Dashboard', [
|
|
104
|
+
'stats' => fn () => Stats::calculate(), // Only loaded when requested
|
|
105
|
+
'activity' => Inertia::lazy(fn () => $user->activity()->latest()->get()),
|
|
106
|
+
]);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## React 19 Side
|
|
110
|
+
|
|
111
|
+
### Page Component Pattern
|
|
112
|
+
```tsx
|
|
113
|
+
import { Head, Link, router } from '@inertiajs/react'
|
|
114
|
+
import { Button } from '@/components/ui/button'
|
|
115
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
116
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
117
|
+
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'
|
|
118
|
+
|
|
119
|
+
interface Product {
|
|
120
|
+
id: number
|
|
121
|
+
name: string
|
|
122
|
+
price: number
|
|
123
|
+
status: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface Props {
|
|
127
|
+
products: {
|
|
128
|
+
data: Product[]
|
|
129
|
+
links: Array<{ url: string | null; label: string; active: boolean }>
|
|
130
|
+
}
|
|
131
|
+
filters: {
|
|
132
|
+
search?: string
|
|
133
|
+
status?: string
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default function ProductsIndex({ products, filters }: Props) {
|
|
138
|
+
function destroy(id: number) {
|
|
139
|
+
if (confirm('Delete this product?')) {
|
|
140
|
+
router.delete(route('products.destroy', id))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<AuthenticatedLayout>
|
|
146
|
+
<Head title="Products" />
|
|
147
|
+
<div className="flex justify-between items-center mb-6">
|
|
148
|
+
<h1 className="text-2xl font-semibold">Products</h1>
|
|
149
|
+
<Button asChild>
|
|
150
|
+
<Link href={route('products.create')}>Add Product</Link>
|
|
151
|
+
</Button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<Card>
|
|
155
|
+
<CardContent className="p-0">
|
|
156
|
+
<Table>
|
|
157
|
+
<TableHeader>
|
|
158
|
+
<TableRow>
|
|
159
|
+
<TableHead>Name</TableHead>
|
|
160
|
+
<TableHead>Price</TableHead>
|
|
161
|
+
<TableHead>Status</TableHead>
|
|
162
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
163
|
+
</TableRow>
|
|
164
|
+
</TableHeader>
|
|
165
|
+
<TableBody>
|
|
166
|
+
{products.data.map((product) => (
|
|
167
|
+
<TableRow key={product.id}>
|
|
168
|
+
<TableCell>{product.name}</TableCell>
|
|
169
|
+
<TableCell>${product.price.toFixed(2)}</TableCell>
|
|
170
|
+
<TableCell>{product.status}</TableCell>
|
|
171
|
+
<TableCell className="text-right space-x-2">
|
|
172
|
+
<Button variant="ghost" size="sm" asChild>
|
|
173
|
+
<Link href={route('products.edit', product.id)}>Edit</Link>
|
|
174
|
+
</Button>
|
|
175
|
+
<Button variant="ghost" size="sm" onClick={() => destroy(product.id)}>
|
|
176
|
+
Delete
|
|
177
|
+
</Button>
|
|
178
|
+
</TableCell>
|
|
179
|
+
</TableRow>
|
|
180
|
+
))}
|
|
181
|
+
</TableBody>
|
|
182
|
+
</Table>
|
|
183
|
+
</CardContent>
|
|
184
|
+
</Card>
|
|
185
|
+
</AuthenticatedLayout>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Form Handling with useForm
|
|
191
|
+
```tsx
|
|
192
|
+
import { useForm } from '@inertiajs/react'
|
|
193
|
+
import { Button } from '@/components/ui/button'
|
|
194
|
+
import { Input } from '@/components/ui/input'
|
|
195
|
+
import { Label } from '@/components/ui/label'
|
|
196
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
197
|
+
|
|
198
|
+
interface Props {
|
|
199
|
+
categories: Record<number, string>
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default function CreateProduct({ categories }: Props) {
|
|
203
|
+
const { data, setData, post, processing, errors, reset } = useForm({
|
|
204
|
+
name: '',
|
|
205
|
+
price: '',
|
|
206
|
+
category_id: '',
|
|
207
|
+
description: '',
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
function submit(e: React.FormEvent) {
|
|
211
|
+
e.preventDefault()
|
|
212
|
+
post(route('products.store'), {
|
|
213
|
+
onSuccess: () => reset(),
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<form onSubmit={submit} className="space-y-4 max-w-lg">
|
|
219
|
+
<div>
|
|
220
|
+
<Label htmlFor="name">Name</Label>
|
|
221
|
+
<Input id="name" value={data.name} onChange={e => setData('name', e.target.value)} />
|
|
222
|
+
{errors.name && <p className="text-sm text-red-500 mt-1">{errors.name}</p>}
|
|
223
|
+
</div>
|
|
224
|
+
<div>
|
|
225
|
+
<Label htmlFor="price">Price</Label>
|
|
226
|
+
<Input id="price" type="number" step="0.01" value={data.price}
|
|
227
|
+
onChange={e => setData('price', e.target.value)} />
|
|
228
|
+
{errors.price && <p className="text-sm text-red-500 mt-1">{errors.price}</p>}
|
|
229
|
+
</div>
|
|
230
|
+
<div>
|
|
231
|
+
<Label>Category</Label>
|
|
232
|
+
<Select value={data.category_id} onValueChange={v => setData('category_id', v)}>
|
|
233
|
+
<SelectTrigger><SelectValue placeholder="Select category" /></SelectTrigger>
|
|
234
|
+
<SelectContent>
|
|
235
|
+
{Object.entries(categories).map(([id, name]) => (
|
|
236
|
+
<SelectItem key={id} value={id}>{name}</SelectItem>
|
|
237
|
+
))}
|
|
238
|
+
</SelectContent>
|
|
239
|
+
</Select>
|
|
240
|
+
{errors.category_id && <p className="text-sm text-red-500 mt-1">{errors.category_id}</p>}
|
|
241
|
+
</div>
|
|
242
|
+
<Button type="submit" disabled={processing}>
|
|
243
|
+
{processing ? 'Saving...' : 'Create Product'}
|
|
244
|
+
</Button>
|
|
245
|
+
</form>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### TypeScript Types
|
|
251
|
+
```typescript
|
|
252
|
+
// resources/js/types/index.d.ts
|
|
253
|
+
export interface User {
|
|
254
|
+
id: number
|
|
255
|
+
name: string
|
|
256
|
+
email: string
|
|
257
|
+
role: 'admin' | 'user'
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
|
|
261
|
+
auth: { user: User }
|
|
262
|
+
flash: { success?: string; error?: string }
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Inertia Router
|
|
267
|
+
```typescript
|
|
268
|
+
import { router } from '@inertiajs/react'
|
|
269
|
+
|
|
270
|
+
router.visit('/products')
|
|
271
|
+
router.get('/products')
|
|
272
|
+
router.post('/products', data)
|
|
273
|
+
router.put('/products/1', data)
|
|
274
|
+
router.patch('/products/1', data)
|
|
275
|
+
router.delete('/products/1')
|
|
276
|
+
|
|
277
|
+
router.post('/products', data, {
|
|
278
|
+
preserveScroll: true,
|
|
279
|
+
preserveState: true,
|
|
280
|
+
only: ['products'],
|
|
281
|
+
onSuccess: () => {},
|
|
282
|
+
onError: (errors) => {},
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
router.reload({ only: ['notifications'] })
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### React 19 Hooks with Inertia
|
|
289
|
+
```tsx
|
|
290
|
+
import { usePage } from '@inertiajs/react'
|
|
291
|
+
|
|
292
|
+
// Access shared data
|
|
293
|
+
const { auth, flash } = usePage<PageProps>().props
|
|
294
|
+
|
|
295
|
+
// Use with React 19 features
|
|
296
|
+
import { useTransition, useState } from 'react'
|
|
297
|
+
|
|
298
|
+
function SearchProducts() {
|
|
299
|
+
const [search, setSearch] = useState('')
|
|
300
|
+
const [isPending, startTransition] = useTransition()
|
|
301
|
+
|
|
302
|
+
function handleSearch(value: string) {
|
|
303
|
+
setSearch(value)
|
|
304
|
+
startTransition(() => {
|
|
305
|
+
router.get('/products', { search: value }, { preserveState: true })
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<Input value={search} onChange={e => handleSearch(e.target.value)}
|
|
311
|
+
placeholder="Search..." className={isPending ? 'opacity-50' : ''} />
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## shadcn/ui Components
|
|
317
|
+
|
|
318
|
+
### Installation
|
|
319
|
+
```bash
|
|
320
|
+
npx shadcn@latest init
|
|
321
|
+
npx shadcn@latest add button card input label select table dialog
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Common Components
|
|
325
|
+
```tsx
|
|
326
|
+
// Button
|
|
327
|
+
import { Button } from '@/components/ui/button'
|
|
328
|
+
<Button>Default</Button>
|
|
329
|
+
<Button variant="secondary">Secondary</Button>
|
|
330
|
+
<Button variant="destructive">Delete</Button>
|
|
331
|
+
<Button variant="outline">Outline</Button>
|
|
332
|
+
<Button variant="ghost">Ghost</Button>
|
|
333
|
+
<Button variant="link">Link</Button>
|
|
334
|
+
<Button size="sm">Small</Button>
|
|
335
|
+
<Button size="lg">Large</Button>
|
|
336
|
+
<Button disabled>Disabled</Button>
|
|
337
|
+
<Button asChild><Link href="/products">Products</Link></Button>
|
|
338
|
+
|
|
339
|
+
// Dialog
|
|
340
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
|
341
|
+
<Dialog>
|
|
342
|
+
<DialogTrigger asChild><Button>Open</Button></DialogTrigger>
|
|
343
|
+
<DialogContent>
|
|
344
|
+
<DialogHeader>
|
|
345
|
+
<DialogTitle>Edit Profile</DialogTitle>
|
|
346
|
+
<DialogDescription>Make changes here.</DialogDescription>
|
|
347
|
+
</DialogHeader>
|
|
348
|
+
{/* form content */}
|
|
349
|
+
<DialogFooter>
|
|
350
|
+
<Button type="submit">Save</Button>
|
|
351
|
+
</DialogFooter>
|
|
352
|
+
</DialogContent>
|
|
353
|
+
</Dialog>
|
|
354
|
+
|
|
355
|
+
// Alert Dialog (confirmation)
|
|
356
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
|
|
357
|
+
<AlertDialog>
|
|
358
|
+
<AlertDialogTrigger asChild><Button variant="destructive">Delete</Button></AlertDialogTrigger>
|
|
359
|
+
<AlertDialogContent>
|
|
360
|
+
<AlertDialogHeader>
|
|
361
|
+
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
362
|
+
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
|
363
|
+
</AlertDialogHeader>
|
|
364
|
+
<AlertDialogFooter>
|
|
365
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
366
|
+
<AlertDialogAction onClick={handleDelete}>Delete</AlertDialogAction>
|
|
367
|
+
</AlertDialogFooter>
|
|
368
|
+
</AlertDialogContent>
|
|
369
|
+
</AlertDialog>
|
|
370
|
+
|
|
371
|
+
// Tabs
|
|
372
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
373
|
+
<Tabs defaultValue="general">
|
|
374
|
+
<TabsList>
|
|
375
|
+
<TabsTrigger value="general">General</TabsTrigger>
|
|
376
|
+
<TabsTrigger value="security">Security</TabsTrigger>
|
|
377
|
+
</TabsList>
|
|
378
|
+
<TabsContent value="general">General settings...</TabsContent>
|
|
379
|
+
<TabsContent value="security">Security settings...</TabsContent>
|
|
380
|
+
</Tabs>
|
|
381
|
+
|
|
382
|
+
// Toast (via sonner)
|
|
383
|
+
import { toast } from 'sonner'
|
|
384
|
+
toast.success('Product saved')
|
|
385
|
+
toast.error('Something went wrong')
|
|
386
|
+
|
|
387
|
+
// Badge
|
|
388
|
+
import { Badge } from '@/components/ui/badge'
|
|
389
|
+
<Badge>Active</Badge>
|
|
390
|
+
<Badge variant="secondary">Pending</Badge>
|
|
391
|
+
<Badge variant="destructive">Cancelled</Badge>
|
|
392
|
+
|
|
393
|
+
// Dropdown Menu
|
|
394
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
|
395
|
+
<DropdownMenu>
|
|
396
|
+
<DropdownMenuTrigger asChild><Button variant="ghost">Actions</Button></DropdownMenuTrigger>
|
|
397
|
+
<DropdownMenuContent>
|
|
398
|
+
<DropdownMenuItem>Edit</DropdownMenuItem>
|
|
399
|
+
<DropdownMenuItem className="text-red-600">Delete</DropdownMenuItem>
|
|
400
|
+
</DropdownMenuContent>
|
|
401
|
+
</DropdownMenu>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## File Structure
|
|
405
|
+
```
|
|
406
|
+
app/
|
|
407
|
+
Http/
|
|
408
|
+
Controllers/
|
|
409
|
+
Middleware/
|
|
410
|
+
HandleInertiaRequests.php
|
|
411
|
+
Requests/
|
|
412
|
+
Models/
|
|
413
|
+
resources/
|
|
414
|
+
js/
|
|
415
|
+
Pages/ # Inertia page components (maps to Inertia::render paths)
|
|
416
|
+
Products/
|
|
417
|
+
Index.tsx
|
|
418
|
+
Create.tsx
|
|
419
|
+
Edit.tsx
|
|
420
|
+
Dashboard.tsx
|
|
421
|
+
Components/ # Reusable React components
|
|
422
|
+
Layouts/
|
|
423
|
+
AuthenticatedLayout.tsx
|
|
424
|
+
GuestLayout.tsx
|
|
425
|
+
hooks/ # Custom React hooks
|
|
426
|
+
lib/ # Utilities, shadcn config
|
|
427
|
+
utils.ts
|
|
428
|
+
types/
|
|
429
|
+
index.d.ts
|
|
430
|
+
app.tsx # React app bootstrap
|
|
431
|
+
routes/
|
|
432
|
+
web.php
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
## Layout Variants (Laravel Starter Kit)
|
|
436
|
+
|
|
437
|
+
### Sidebar Layout (default)
|
|
438
|
+
```tsx
|
|
439
|
+
// resources/js/layouts/app-layout.tsx
|
|
440
|
+
import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout'
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Header Layout
|
|
444
|
+
```tsx
|
|
445
|
+
import AppLayoutTemplate from '@/layouts/app/app-header-layout'
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Sidebar Variants
|
|
449
|
+
```tsx
|
|
450
|
+
<Sidebar collapsible="icon" variant="sidebar"> // default
|
|
451
|
+
<Sidebar collapsible="icon" variant="inset"> // inset
|
|
452
|
+
<Sidebar collapsible="icon" variant="floating"> // floating
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Auth Layout Variants
|
|
456
|
+
```tsx
|
|
457
|
+
import AuthLayoutTemplate from '@/layouts/auth/auth-simple-layout' // simple
|
|
458
|
+
import AuthLayoutTemplate from '@/layouts/auth/auth-split-layout' // split
|
|
459
|
+
// default: card layout
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
## Type Sync Pattern (Backend to Frontend)
|
|
463
|
+
|
|
464
|
+
When changing data sent from Laravel to React:
|
|
465
|
+
1. **Migration**: add column
|
|
466
|
+
2. **Controller**: add to `Inertia::render()` select/only
|
|
467
|
+
3. **TypeScript**: update interface in `types/`
|
|
468
|
+
4. **React component**: update Props and render
|
|
469
|
+
|
|
470
|
+
## Common Gotchas
|
|
471
|
+
|
|
472
|
+
1. **No React Router**: Inertia replaces client routing. Use `<Link>` and `router` from `@inertiajs/react`.
|
|
473
|
+
2. **Props reset on navigation**: Inertia replaces the entire page component. Local `useState` resets. Use `preserveState: true` or a state manager.
|
|
474
|
+
3. **Validation errors**: auto-available via `useForm().errors`. No manual error handling needed.
|
|
475
|
+
4. **Flash messages**: use `->with('success', 'msg')` on redirects. Access via shared data.
|
|
476
|
+
5. **File uploads**: `useForm` handles multipart automatically when data contains File objects.
|
|
477
|
+
6. **Ziggy routes**: use `route('name')` in React via Ziggy. Never hardcode URLs.
|
|
478
|
+
7. **Partial reloads**: wrap expensive props in closures `fn ()` in controller. Request with `router.reload({ only: ['prop'] })`.
|
|
479
|
+
8. **SSR**: supported via `@inertiajs/react/server`. Build with `npm run build:ssr`.
|
|
480
|
+
9. **shadcn/ui install**: each component must be added individually with `npx shadcn@latest add <name>`.
|
|
481
|
+
10. **Tailwind v4**: uses CSS-based config, not `tailwind.config.js`. CSS layers matter for component overrides.
|