@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 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.