start-vibing-stacks 2.8.0 → 2.10.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.
@@ -0,0 +1,509 @@
1
+ ---
2
+ name: react-api-standards
3
+ version: 1.0.0
4
+ description: React 19 + Vite + Axios standards for a Laravel-backed JSON API SPA
5
+ — Page shell + skeleton, async data via `api.get`, form 422 binding, route
6
+ catch-all, no Inertia. Use for any new React page in a Laravel + Sanctum SPA
7
+ project. Pairs with `axios-laravel-api` and `laravel-api-architecture`.
8
+ ---
9
+
10
+ # React 19 API-First Standards (Laravel + Axios)
11
+
12
+ **ALWAYS invoke when creating React pages, components, or layouts in a project
13
+ that uses a Laravel JSON API as backend (NOT Inertia.js).**
14
+
15
+ ## Version Requirements
16
+
17
+ - **React >= 19** — MANDATORY (`useActionState`, `useOptimistic`, `use`)
18
+ - **Vite >= 5** with `@vitejs/plugin-react`
19
+ - **TailwindCSS >= 4**
20
+ - **Axios >= 1.6** (for `withXSRFToken` support)
21
+ - **TanStack Query >= 5** (recommended for cached lists/details)
22
+ - **react-router-dom >= 6** (client-side routing)
23
+
24
+ ## Folder Structure
25
+
26
+ ```
27
+ resources/js/
28
+ ├── app.jsx # Root: <BrowserRouter> + <QueryClientProvider>
29
+ ├── App.jsx # Top-level <Routes> with <ProtectedRoute>
30
+ ├── lib/
31
+ │ ├── api.js # Axios instance (see axios-laravel-api skill)
32
+ │ ├── auth.js # login/logout/fetchCurrentUser
33
+ │ ├── queryClient.ts # TanStack QueryClient config
34
+ │ └── utils.ts # cn(), formatters, date helpers
35
+ ├── Pages/
36
+ │ ├── Dashboard/
37
+ │ │ └── Index.tsx # Renders shell+skeleton; calls api.get
38
+ │ ├── Users/
39
+ │ │ ├── Index.tsx
40
+ │ │ ├── Show.tsx
41
+ │ │ └── _components/ # Page-private components
42
+ │ │ ├── UsersTable.tsx
43
+ │ │ └── UsersTableSkeleton.tsx
44
+ │ └── Auth/
45
+ │ ├── Login.tsx
46
+ │ └── Register.tsx
47
+ ├── Components/ # Reusable cross-page components
48
+ │ ├── ErrorState.tsx
49
+ │ ├── EmptyState.tsx
50
+ │ ├── ProtectedRoute.tsx
51
+ │ └── SectionLoader.tsx
52
+ ├── Layouts/
53
+ │ ├── AuthenticatedLayout.tsx
54
+ │ └── GuestLayout.tsx
55
+ ├── Icons/ # Each icon is a separate .svg file
56
+ │ ├── index.js # Barrel
57
+ │ ├── CheckIcon.svg
58
+ │ └── AlertIcon.svg
59
+ └── store/ # Zustand / Context for cross-page UI state
60
+ └── auth.ts
61
+ ```
62
+
63
+ ## The Page Contract
64
+
65
+ Every Page component MUST:
66
+
67
+ 1. **Render shell + skeleton on first paint** — never block on data.
68
+ 2. **Fetch via `api` instance** (or `useQuery`) — never raw `axios`/`fetch`.
69
+ 3. **Show error/empty/loading states** before the happy path.
70
+ 4. **Define `LABELS` and `STYLES` as `const` ABOVE the component** — stable refs.
71
+
72
+ ```tsx
73
+ import { useEffect, useState } from 'react';
74
+ import api from '@/lib/api';
75
+ import ErrorState from '@/Components/ErrorState';
76
+ import EmptyState from '@/Components/EmptyState';
77
+ import OrdersTable from './_components/OrdersTable';
78
+ import OrdersTableSkeleton from './_components/OrdersTableSkeleton';
79
+
80
+ const LABELS = {
81
+ title: 'Orders',
82
+ empty: 'No orders yet',
83
+ emptyHint: 'Orders will appear here once customers start checking out.',
84
+ } as const;
85
+
86
+ const STYLES = {
87
+ page: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8',
88
+ heading: 'text-2xl font-bold text-foreground',
89
+ section: 'mt-6',
90
+ } as const;
91
+
92
+ export default function OrdersIndexPage() {
93
+ const [data, setData] = useState<{ data: Order[]; meta: Meta } | null>(null);
94
+ const [error, setError] = useState<string | null>(null);
95
+ const [loading, setLoading] = useState(true);
96
+
97
+ const load = async () => {
98
+ try {
99
+ setError(null);
100
+ setLoading(true);
101
+ const res = await api.get('/api/orders');
102
+ setData(res.data);
103
+ } catch (e: any) {
104
+ setError(e.response?.data?.message ?? 'Failed to load orders');
105
+ } finally {
106
+ setLoading(false);
107
+ }
108
+ };
109
+
110
+ useEffect(() => { load(); }, []);
111
+
112
+ return (
113
+ <div className={STYLES.page}>
114
+ <h1 className={STYLES.heading}>{LABELS.title}</h1>
115
+
116
+ <section className={STYLES.section}>
117
+ {error && <ErrorState error={error} onRetry={load} />}
118
+ {!error && loading && !data && <OrdersTableSkeleton />}
119
+ {!error && data && data.data.length === 0 && (
120
+ <EmptyState title={LABELS.empty} description={LABELS.emptyHint} />
121
+ )}
122
+ {!error && data && data.data.length > 0 && (
123
+ <OrdersTable orders={data.data} meta={data.meta} />
124
+ )}
125
+ </section>
126
+ </div>
127
+ );
128
+ }
129
+ ```
130
+
131
+ ## TanStack Query Variant (Preferred for any List/Detail)
132
+
133
+ ```tsx
134
+ import { useQuery } from '@tanstack/react-query';
135
+ import { useSearchParams } from 'react-router-dom';
136
+ import api from '@/lib/api';
137
+
138
+ const fetchOrders = async (params: { page: number; per_page: number; status?: string }) => {
139
+ const { data } = await api.get('/api/orders', { params });
140
+ return data;
141
+ };
142
+
143
+ export default function OrdersIndexPage() {
144
+ const [searchParams, setSearchParams] = useSearchParams();
145
+ const page = Number(searchParams.get('page') ?? 1);
146
+ const status = searchParams.get('status') ?? undefined;
147
+
148
+ const { data, error, isPending, refetch } = useQuery({
149
+ queryKey: ['orders', { page, status }],
150
+ queryFn: () => fetchOrders({ page, per_page: 25, status }),
151
+ placeholderData: (prev) => prev, // smooth pagination
152
+ });
153
+
154
+ if (error) return <ErrorState error={error} onRetry={refetch} />;
155
+ if (isPending && !data) return <OrdersTableSkeleton />;
156
+ if (!data?.data.length) return <EmptyState title={LABELS.empty} />;
157
+
158
+ return (
159
+ <OrdersTable
160
+ orders={data.data}
161
+ meta={data.meta}
162
+ onPageChange={(p) => setSearchParams({ page: String(p), status: status ?? '' })}
163
+ />
164
+ );
165
+ }
166
+ ```
167
+
168
+ **Rules:**
169
+
170
+ - `queryKey` MUST include all filter inputs that change the response.
171
+ - `placeholderData: (prev) => prev` keeps the old table visible while the new
172
+ page loads — no skeleton flash on pagination.
173
+ - URL state via `useSearchParams` so users can bookmark filtered views.
174
+
175
+ ## Mutations + Optimistic UI
176
+
177
+ ```tsx
178
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
179
+ import api from '@/lib/api';
180
+
181
+ function ToggleArchive({ order }: { order: Order }) {
182
+ const qc = useQueryClient();
183
+ const m = useMutation({
184
+ mutationFn: () => api.post(`/api/orders/${order.id}/archive`),
185
+ onMutate: async () => {
186
+ await qc.cancelQueries({ queryKey: ['orders'] });
187
+ const prev = qc.getQueriesData<any>({ queryKey: ['orders'] });
188
+ qc.setQueriesData({ queryKey: ['orders'] }, (old: any) => ({
189
+ ...old,
190
+ data: old.data.map((o: Order) =>
191
+ o.id === order.id ? { ...o, archived: !o.archived } : o,
192
+ ),
193
+ }));
194
+ return { prev };
195
+ },
196
+ onError: (_err, _vars, ctx) => {
197
+ ctx?.prev?.forEach(([key, value]) => qc.setQueryData(key, value));
198
+ },
199
+ onSettled: () => qc.invalidateQueries({ queryKey: ['orders'] }),
200
+ });
201
+ return <button onClick={() => m.mutate()} disabled={m.isPending}>Archive</button>;
202
+ }
203
+ ```
204
+
205
+ ## Forms — 422 Validation Binding
206
+
207
+ ```tsx
208
+ import { useState } from 'react';
209
+ import api from '@/lib/api';
210
+
211
+ const STYLES = {
212
+ form: 'space-y-4 max-w-md',
213
+ label: 'block text-sm font-medium text-foreground mb-1',
214
+ input: 'w-full h-10 px-3 rounded-md border border-border bg-background',
215
+ inputError: 'w-full h-10 px-3 rounded-md border border-destructive bg-background',
216
+ error: 'mt-1 text-sm text-destructive',
217
+ btn: 'px-4 py-2 bg-primary text-primary-foreground rounded-lg disabled:opacity-50',
218
+ } as const;
219
+
220
+ export default function CreateOrderForm({ onCreated }: { onCreated: () => void }) {
221
+ const [form, setForm] = useState({ product_id: '', quantity: 1 });
222
+ const [errors, setErrors] = useState<Record<string, string[]>>({});
223
+ const [submitting, setSubmitting] = useState(false);
224
+
225
+ const submit = async (e: React.FormEvent) => {
226
+ e.preventDefault();
227
+ setErrors({});
228
+ setSubmitting(true);
229
+ try {
230
+ const { data } = await api.post('/api/orders', form);
231
+ onCreated();
232
+ } catch (err: any) {
233
+ if (err.validation) setErrors(err.validation);
234
+ } finally {
235
+ setSubmitting(false);
236
+ }
237
+ };
238
+
239
+ const cls = (field: string) =>
240
+ errors[field]?.length ? STYLES.inputError : STYLES.input;
241
+
242
+ return (
243
+ <form className={STYLES.form} onSubmit={submit}>
244
+ <div>
245
+ <label className={STYLES.label}>Product ID</label>
246
+ <input
247
+ className={cls('product_id')}
248
+ value={form.product_id}
249
+ onChange={(e) => setForm({ ...form, product_id: e.target.value })}
250
+ />
251
+ {errors.product_id?.[0] && (
252
+ <p className={STYLES.error}>{errors.product_id[0]}</p>
253
+ )}
254
+ </div>
255
+
256
+ <button type="submit" disabled={submitting} className={STYLES.btn}>
257
+ {submitting ? 'Creating…' : 'Create Order'}
258
+ </button>
259
+ </form>
260
+ );
261
+ }
262
+ ```
263
+
264
+ ## Authentication Layer
265
+
266
+ ```tsx
267
+ // resources/js/store/auth.ts
268
+ import { create } from 'zustand';
269
+ import api from '@/lib/api';
270
+ import { fetchCurrentUser, login as doLogin, logout as doLogout } from '@/lib/auth';
271
+
272
+ interface AuthState {
273
+ user: User | null;
274
+ booted: boolean;
275
+ boot: () => Promise<void>;
276
+ login: (email: string, password: string) => Promise<void>;
277
+ logout: () => Promise<void>;
278
+ }
279
+
280
+ export const useAuth = create<AuthState>((set) => ({
281
+ user: null,
282
+ booted: false,
283
+ boot: async () => {
284
+ try {
285
+ const user = await fetchCurrentUser();
286
+ set({ user, booted: true });
287
+ } catch {
288
+ set({ user: null, booted: true });
289
+ }
290
+ },
291
+ login: async (email, password) => {
292
+ await doLogin(email, password);
293
+ const user = await fetchCurrentUser();
294
+ set({ user });
295
+ },
296
+ logout: async () => {
297
+ await doLogout();
298
+ set({ user: null });
299
+ },
300
+ }));
301
+ ```
302
+
303
+ ```tsx
304
+ // resources/js/Components/ProtectedRoute.tsx
305
+ import { Navigate, useLocation } from 'react-router-dom';
306
+ import { useAuth } from '@/store/auth';
307
+ import SectionLoader from './SectionLoader';
308
+
309
+ export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
310
+ const { user, booted } = useAuth();
311
+ const location = useLocation();
312
+
313
+ if (!booted) return <SectionLoader />;
314
+ if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
315
+
316
+ return <>{children}</>;
317
+ }
318
+ ```
319
+
320
+ ```tsx
321
+ // resources/js/App.tsx
322
+ import { useEffect } from 'react';
323
+ import { Routes, Route } from 'react-router-dom';
324
+ import { useAuth } from './store/auth';
325
+ import ProtectedRoute from './Components/ProtectedRoute';
326
+ import AuthenticatedLayout from './Layouts/AuthenticatedLayout';
327
+ import LoginPage from './Pages/Auth/Login';
328
+ import DashboardPage from './Pages/Dashboard/Index';
329
+
330
+ export default function App() {
331
+ const boot = useAuth((s) => s.boot);
332
+ useEffect(() => { boot(); }, [boot]);
333
+
334
+ return (
335
+ <Routes>
336
+ <Route path="/login" element={<LoginPage />} />
337
+ <Route element={<ProtectedRoute><AuthenticatedLayout /></ProtectedRoute>}>
338
+ <Route path="/" element={<DashboardPage />} />
339
+ {/* ... */}
340
+ </Route>
341
+ </Routes>
342
+ );
343
+ }
344
+ ```
345
+
346
+ ## Skeletons — One per Component, Match the Shape
347
+
348
+ ```tsx
349
+ // resources/js/Pages/Orders/_components/OrdersTableSkeleton.tsx
350
+ const STYLES = {
351
+ table: 'w-full',
352
+ headerRow: 'border-b border-border',
353
+ headerCell: 'h-4 w-24 bg-muted rounded animate-pulse my-3 mx-4',
354
+ row: 'border-b border-border',
355
+ cell: 'h-4 w-full bg-muted rounded animate-pulse my-4 mx-4',
356
+ } as const;
357
+
358
+ export default function OrdersTableSkeleton({ rows = 8 }: { rows?: number }) {
359
+ return (
360
+ <div role="status" aria-label="Loading orders" className={STYLES.table}>
361
+ <div className={STYLES.headerRow}>
362
+ {Array.from({ length: 4 }).map((_, i) => (
363
+ <div key={i} className={STYLES.headerCell} />
364
+ ))}
365
+ </div>
366
+ {Array.from({ length: rows }).map((_, r) => (
367
+ <div key={r} className={STYLES.row}>
368
+ {Array.from({ length: 4 }).map((_, c) => (
369
+ <div key={c} className={STYLES.cell} />
370
+ ))}
371
+ </div>
372
+ ))}
373
+ </div>
374
+ );
375
+ }
376
+ ```
377
+
378
+ ## Error & Empty States (Reusable)
379
+
380
+ ```tsx
381
+ // resources/js/Components/ErrorState.tsx
382
+ const STYLES = {
383
+ wrap: 'flex flex-col items-center justify-center py-12 text-center',
384
+ icon: 'h-12 w-12 rounded-full bg-destructive/10 flex items-center justify-center mb-4',
385
+ title: 'text-lg font-semibold text-foreground',
386
+ msg: 'mt-1 text-sm text-muted-foreground max-w-md',
387
+ btn: 'mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg',
388
+ } as const;
389
+
390
+ export default function ErrorState({
391
+ error, onRetry, title = 'Something went wrong',
392
+ }: { error: unknown; onRetry?: () => void; title?: string }) {
393
+ const message = typeof error === 'string'
394
+ ? error
395
+ : (error as any)?.message ?? 'Please try again.';
396
+ return (
397
+ <div className={STYLES.wrap}>
398
+ <div className={STYLES.icon}>!</div>
399
+ <h3 className={STYLES.title}>{title}</h3>
400
+ <p className={STYLES.msg}>{message}</p>
401
+ {onRetry && <button className={STYLES.btn} onClick={onRetry}>Try again</button>}
402
+ </div>
403
+ );
404
+ }
405
+ ```
406
+
407
+ ## TypeScript — Shape Types from Resources
408
+
409
+ ```ts
410
+ // resources/js/types/index.d.ts
411
+
412
+ export interface PaginatedResponse<T> {
413
+ data: T[];
414
+ meta: {
415
+ current_page: number;
416
+ last_page: number;
417
+ per_page: number;
418
+ total: number;
419
+ };
420
+ links: {
421
+ first: string | null;
422
+ last: string | null;
423
+ next: string | null;
424
+ prev: string | null;
425
+ };
426
+ }
427
+
428
+ export interface User {
429
+ id: string;
430
+ name: string;
431
+ email: string;
432
+ role: 'user' | 'admin' | 'superadmin';
433
+ created_at: string;
434
+ updated_at: string;
435
+ }
436
+
437
+ export interface Order {
438
+ id: string;
439
+ product_id: string;
440
+ quantity: number;
441
+ status: 'pending' | 'paid' | 'shipped' | 'cancelled';
442
+ created_at: string;
443
+ }
444
+ ```
445
+
446
+ **Rule:** Types live in `resources/js/types/`, NOT scattered in components.
447
+ Match the field names from your Laravel API Resources exactly.
448
+
449
+ ## Path Aliases (`@/...`)
450
+
451
+ ```js
452
+ // vite.config.js
453
+ import path from 'path';
454
+
455
+ resolve: {
456
+ alias: {
457
+ '@': path.resolve(__dirname, 'resources/js'),
458
+ },
459
+ },
460
+ ```
461
+
462
+ ```json
463
+ // tsconfig.json
464
+ {
465
+ "compilerOptions": {
466
+ "baseUrl": ".",
467
+ "paths": {
468
+ "@/*": ["resources/js/*"]
469
+ }
470
+ }
471
+ }
472
+ ```
473
+
474
+ ## Checklist — Before Shipping a Page
475
+
476
+ - [ ] Renders shell + heading on first paint (no waterfall blocking)
477
+ - [ ] Skeleton matches the shape of the loaded content
478
+ - [ ] Error state with retry handler
479
+ - [ ] Empty state for collections
480
+ - [ ] All API calls go through `@/lib/api` (interceptors apply)
481
+ - [ ] Form errors bound from `error.validation` (422)
482
+ - [ ] LABELS + STYLES defined as `const` above the component
483
+ - [ ] Page is wrapped in `<ProtectedRoute>` if auth required
484
+ - [ ] Filters reflected in URL via `useSearchParams`
485
+
486
+ ## FORBIDDEN
487
+
488
+ | Pattern | Reason | Use Instead |
489
+ |--------|--------|-------------|
490
+ | `Inertia::render()` for new pages | Blocks first paint on DB | `api.get()` from React |
491
+ | `fetch()` in components | Bypasses CSRF/interceptors | `api.get/post/...` |
492
+ | `axios.get(...)` direct | Bypasses interceptors | `import api from '@/lib/api'` |
493
+ | `useEffect` to derive state | Anti-pattern | `useMemo` |
494
+ | `<a href>` for internal links | Full reload | `<Link>` from react-router-dom |
495
+ | `window.location.assign` for SPA nav | Full reload | `useNavigate()` |
496
+ | `localStorage` for tokens | XSS-readable | HttpOnly session cookie (Sanctum) |
497
+ | Skipping skeleton/empty/error states | Bad UX | All three are mandatory per page |
498
+ | Inline Tailwind class soup | Unstable refs | `STYLES` const at top |
499
+ | `__()`-style i18n inside JSX | React Hook violation | LABELS const at top |
500
+ | `dd()`-style debug | n/a in JS | Controlled debug constant pattern |
501
+ | Storing API tokens in `VITE_*` | Bundled = public | Server-side via session/Sanctum |
502
+
503
+ ## See Also
504
+
505
+ - `axios-laravel-api` — the `api.js` instance and Sanctum CSRF flow
506
+ - `laravel-api-architecture` — the backend Controller→Service→Resource pipeline
507
+ - `react-patterns` — generic React 19 hooks (`useActionState`, `useOptimistic`)
508
+ - `react-ui-patterns` — loading/error/empty patterns and decision tree
509
+ - `tailwind-patterns` — utility class composition with `cn()`
@@ -1,9 +1,18 @@
1
1
  ---
2
2
  name: inertia-react
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: LEGACY skill — Inertia.js + React frontend integration with
5
+ Laravel-rendered pages. Use ONLY in pre-existing Inertia projects. For NEW
6
+ projects use the `react-api` frontend stack (`axios-laravel-api` +
7
+ `react-api-standards`) which decouples render from data fetching.
4
8
  ---
5
9
 
6
- # Inertia.js + React Integration
10
+ # Inertia.js + React Integration (LEGACY)
11
+
12
+ > **STATUS: LEGACY.** New projects use the **React API SPA** stack — Laravel
13
+ > serves a Vite shell, React owns routing client-side, and data is fetched via
14
+ > Axios with Sanctum cookie auth. See `axios-laravel-api` and
15
+ > `react-api-standards`.
7
16
 
8
17
  ## Architecture Overview
9
18
 
@@ -1,9 +1,16 @@
1
1
  ---
2
2
  name: react-standards
3
- version: 1.0.0
3
+ version: 2.0.0
4
+ description: LEGACY — React 19 standards for Inertia.js projects (controller
5
+ props, useForm, Inertia router). Use ONLY in pre-existing Inertia projects.
6
+ For NEW projects use `react-api-standards` (Axios + TanStack Query + React
7
+ Router) instead.
4
8
  ---
5
9
 
6
- # React 19+ Standards (with Inertia.js)
10
+ # React 19+ Standards with Inertia.js (LEGACY)
11
+
12
+ > **STATUS: LEGACY.** New projects use `react-api-standards` (API-first SPA).
13
+ > Keep this loaded only for legacy Inertia codebases.
7
14
 
8
15
  ## Version Requirements
9
16