@kennethsolomon/shipkit 3.2.0 → 3.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 +7 -1
- package/package.json +1 -1
- package/skills/sk:e2e/SKILL.md +161 -10
- package/skills/sk:mvp/SKILL.md +266 -0
- package/skills/sk:mvp/references/design-system.md +136 -0
- package/skills/sk:mvp/references/landing-page.md +236 -0
- package/skills/sk:mvp/references/stacks/laravel.md +321 -0
- package/skills/sk:mvp/references/stacks/nextjs.md +189 -0
- package/skills/sk:mvp/references/stacks/nuxt.md +250 -0
- package/skills/sk:mvp/references/stacks/react-vite.md +287 -0
- package/skills/sk:write-tests/SKILL.md +42 -3
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# React + Vite + Tailwind — Stack Reference
|
|
2
|
+
|
|
3
|
+
## Scaffold
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create vite@latest {project-name} -- --template react-ts
|
|
7
|
+
cd {project-name}
|
|
8
|
+
npm install
|
|
9
|
+
npm install -D tailwindcss @tailwindcss/vite
|
|
10
|
+
npm install react-router-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Directory Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{project-name}/
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── main.tsx ← entry point (router setup)
|
|
19
|
+
│ ├── App.tsx ← route definitions
|
|
20
|
+
│ ├── index.css ← Tailwind directives + custom CSS
|
|
21
|
+
│ ├── pages/
|
|
22
|
+
│ │ ├── Landing.tsx ← landing page
|
|
23
|
+
│ │ ├── Dashboard.tsx ← dashboard
|
|
24
|
+
│ │ ├── {Feature1}.tsx
|
|
25
|
+
│ │ ├── {Feature2}.tsx
|
|
26
|
+
│ │ └── Settings.tsx
|
|
27
|
+
│ ├── components/
|
|
28
|
+
│ │ ├── landing/
|
|
29
|
+
│ │ │ ├── Navbar.tsx
|
|
30
|
+
│ │ │ ├── Hero.tsx
|
|
31
|
+
│ │ │ ├── Features.tsx
|
|
32
|
+
│ │ │ ├── HowItWorks.tsx
|
|
33
|
+
│ │ │ ├── Pricing.tsx
|
|
34
|
+
│ │ │ ├── Testimonials.tsx
|
|
35
|
+
│ │ │ ├── WaitlistForm.tsx
|
|
36
|
+
│ │ │ └── Footer.tsx
|
|
37
|
+
│ │ ├── app/
|
|
38
|
+
│ │ │ ├── Sidebar.tsx
|
|
39
|
+
│ │ │ ├── AppLayout.tsx
|
|
40
|
+
│ │ │ ├── DashboardCards.tsx
|
|
41
|
+
│ │ │ └── {feature components}
|
|
42
|
+
│ │ └── ui/
|
|
43
|
+
│ │ ├── Button.tsx
|
|
44
|
+
│ │ ├── Input.tsx
|
|
45
|
+
│ │ ├── Card.tsx
|
|
46
|
+
│ │ ├── Modal.tsx
|
|
47
|
+
│ │ └── Toast.tsx
|
|
48
|
+
│ ├── data/
|
|
49
|
+
│ │ └── mock.ts ← all fake data centralized
|
|
50
|
+
│ └── lib/
|
|
51
|
+
│ └── utils.ts ← shared helpers (cn, etc.)
|
|
52
|
+
├── public/
|
|
53
|
+
│ └── {static assets}
|
|
54
|
+
├── index.html
|
|
55
|
+
├── vite.config.ts
|
|
56
|
+
├── tailwind.config.ts
|
|
57
|
+
└── package.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Vite Config
|
|
61
|
+
|
|
62
|
+
`vite.config.ts`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { defineConfig } from 'vite'
|
|
66
|
+
import react from '@vitejs/plugin-react'
|
|
67
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
plugins: [react(), tailwindcss()],
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tailwind Config
|
|
75
|
+
|
|
76
|
+
`tailwind.config.ts`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export default {
|
|
80
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
81
|
+
theme: {
|
|
82
|
+
extend: {
|
|
83
|
+
colors: {
|
|
84
|
+
bg: 'var(--color-bg)',
|
|
85
|
+
fg: 'var(--color-fg)',
|
|
86
|
+
accent: 'var(--color-accent)',
|
|
87
|
+
muted: 'var(--color-muted)',
|
|
88
|
+
},
|
|
89
|
+
fontFamily: {
|
|
90
|
+
display: ['var(--font-display)', 'serif'],
|
|
91
|
+
body: ['var(--font-body)', 'sans-serif'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
CSS variables and font imports in `src/index.css`:
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
@import url('https://fonts.googleapis.com/css2?family={DisplayFont}:wght@400;600;700;800&family={BodyFont}:wght@400;500;600&display=swap');
|
|
102
|
+
|
|
103
|
+
@tailwind base;
|
|
104
|
+
@tailwind components;
|
|
105
|
+
@tailwind utilities;
|
|
106
|
+
|
|
107
|
+
:root {
|
|
108
|
+
--color-bg: #xxxxxx;
|
|
109
|
+
--color-fg: #xxxxxx;
|
|
110
|
+
--color-accent: #xxxxxx;
|
|
111
|
+
--color-muted: #xxxxxx;
|
|
112
|
+
--font-display: '{DisplayFont}', serif;
|
|
113
|
+
--font-body: '{BodyFont}', sans-serif;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
body {
|
|
117
|
+
font-family: var(--font-body);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Router Setup
|
|
122
|
+
|
|
123
|
+
`src/App.tsx`:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
127
|
+
import Landing from './pages/Landing'
|
|
128
|
+
import AppLayout from './components/app/AppLayout'
|
|
129
|
+
import Dashboard from './pages/Dashboard'
|
|
130
|
+
import Feature1 from './pages/{Feature1}'
|
|
131
|
+
import Feature2 from './pages/{Feature2}'
|
|
132
|
+
import Settings from './pages/Settings'
|
|
133
|
+
|
|
134
|
+
const router = createBrowserRouter([
|
|
135
|
+
{ path: '/', element: <Landing /> },
|
|
136
|
+
{
|
|
137
|
+
element: <AppLayout />,
|
|
138
|
+
children: [
|
|
139
|
+
{ path: '/dashboard', element: <Dashboard /> },
|
|
140
|
+
{ path: '/{feature-1}', element: <Feature1 /> },
|
|
141
|
+
{ path: '/{feature-2}', element: <Feature2 /> },
|
|
142
|
+
{ path: '/settings', element: <Settings /> },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
export default function App() {
|
|
148
|
+
return <RouterProvider router={router} />
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## App Layout
|
|
153
|
+
|
|
154
|
+
`src/components/app/AppLayout.tsx`:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Outlet } from 'react-router-dom'
|
|
158
|
+
import Sidebar from './Sidebar'
|
|
159
|
+
|
|
160
|
+
export default function AppLayout() {
|
|
161
|
+
return (
|
|
162
|
+
<div className="flex min-h-screen bg-bg text-fg">
|
|
163
|
+
<Sidebar />
|
|
164
|
+
<main className="flex-1 p-6 lg:p-8">
|
|
165
|
+
<Outlet />
|
|
166
|
+
</main>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Waitlist — Formspree Integration
|
|
173
|
+
|
|
174
|
+
Since React + Vite has no backend, use Formspree for email collection.
|
|
175
|
+
|
|
176
|
+
`src/components/landing/WaitlistForm.tsx`:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { useState, FormEvent } from 'react'
|
|
180
|
+
|
|
181
|
+
// Replace YOUR_FORM_ID with your Formspree form ID
|
|
182
|
+
// Create one free at https://formspree.io
|
|
183
|
+
const FORMSPREE_URL = 'https://formspree.io/f/YOUR_FORM_ID'
|
|
184
|
+
|
|
185
|
+
export default function WaitlistForm() {
|
|
186
|
+
const [email, setEmail] = useState('')
|
|
187
|
+
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
188
|
+
const [message, setMessage] = useState('')
|
|
189
|
+
|
|
190
|
+
async function handleSubmit(e: FormEvent) {
|
|
191
|
+
e.preventDefault()
|
|
192
|
+
setStatus('loading')
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(FORMSPREE_URL, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
198
|
+
body: JSON.stringify({ email }),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (res.ok) {
|
|
202
|
+
setStatus('success')
|
|
203
|
+
setMessage("You're on the list! We'll notify you when we launch.")
|
|
204
|
+
} else {
|
|
205
|
+
throw new Error()
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
setStatus('error')
|
|
209
|
+
setMessage('Something went wrong. Please try again.')
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (status === 'success') {
|
|
214
|
+
return <p className="text-green-600 font-medium text-lg">{message}</p>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3 max-w-md">
|
|
219
|
+
<input
|
|
220
|
+
type="email"
|
|
221
|
+
value={email}
|
|
222
|
+
onChange={e => setEmail(e.target.value)}
|
|
223
|
+
placeholder="you@example.com"
|
|
224
|
+
required
|
|
225
|
+
className="flex-1 px-4 py-3 rounded-lg border border-muted/30 bg-bg focus:ring-2 focus:ring-accent focus:outline-none"
|
|
226
|
+
/>
|
|
227
|
+
<button
|
|
228
|
+
type="submit"
|
|
229
|
+
disabled={status === 'loading'}
|
|
230
|
+
className="px-6 py-3 bg-accent text-white rounded-xl font-medium hover:opacity-90 transition-all disabled:opacity-50"
|
|
231
|
+
>
|
|
232
|
+
{status === 'loading' ? 'Joining...' : 'Join Waitlist'}
|
|
233
|
+
</button>
|
|
234
|
+
{status === 'error' && <p className="text-red-500 text-sm">{message}</p>}
|
|
235
|
+
</form>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Mock Data
|
|
241
|
+
|
|
242
|
+
Centralize all fake data in `src/data/mock.ts`:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
export const features = [
|
|
246
|
+
{ icon: '🎯', title: 'Feature One', description: 'Short benefit-driven description.' },
|
|
247
|
+
{ icon: '⚡', title: 'Feature Two', description: 'Short benefit-driven description.' },
|
|
248
|
+
{ icon: '🔒', title: 'Feature Three', description: 'Short benefit-driven description.' },
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
export const testimonials = [
|
|
252
|
+
{ quote: 'Realistic testimonial here.', name: 'Jane Smith', role: 'CTO, TechCo' },
|
|
253
|
+
// ...
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
export const pricingPlans = [
|
|
257
|
+
{ name: 'Free', price: '$0', features: ['Feature A', 'Feature B'], cta: 'Get Started' },
|
|
258
|
+
{ name: 'Pro', price: '$29/mo', features: ['Everything in Free', 'Feature C'], cta: 'Get Pro', popular: true },
|
|
259
|
+
{ name: 'Enterprise', price: 'Custom', features: ['Everything in Pro', 'Priority support'], cta: 'Contact Us' },
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
// Dashboard mock data
|
|
263
|
+
export const dashboardStats = [
|
|
264
|
+
{ label: 'Total Users', value: '2,847', change: '+12%' },
|
|
265
|
+
// ...
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
export const recentActivity = [
|
|
269
|
+
{ user: 'Jane Smith', action: 'created a new project', time: '2 hours ago' },
|
|
270
|
+
// ...
|
|
271
|
+
]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Component Patterns
|
|
275
|
+
|
|
276
|
+
- Use functional components with TypeScript.
|
|
277
|
+
- Use `useState` for local state, no state management library needed.
|
|
278
|
+
- Navigation: `<Link to="/dashboard">` from react-router-dom.
|
|
279
|
+
- Keep components focused — one file per component.
|
|
280
|
+
- Props typed with interfaces or inline.
|
|
281
|
+
|
|
282
|
+
## Dev Server
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npm run dev
|
|
286
|
+
# Runs on http://localhost:5173
|
|
287
|
+
```
|
|
@@ -145,16 +145,54 @@ If a frontend stack was detected, generate FE test files:
|
|
|
145
145
|
|
|
146
146
|
Skip this step if no FE stack was detected.
|
|
147
147
|
|
|
148
|
-
### 8b.
|
|
148
|
+
### 8b. Write E2E Spec Files (conditional)
|
|
149
|
+
|
|
150
|
+
**Only if `playwright.config.ts` or `playwright.config.js` is detected in the project root:**
|
|
151
|
+
|
|
152
|
+
Write `e2e/<feature>.spec.ts` files covering the acceptance criteria from `tasks/todo.md`. Follow these rules:
|
|
153
|
+
|
|
154
|
+
- Use `test.describe` / `test` blocks — not `describe`/`it`
|
|
155
|
+
- Use role-based locators: `getByRole`, `getByLabel`, `getByText`, `getByPlaceholder` — never CSS selectors
|
|
156
|
+
- Use `test.beforeEach` for shared setup (auth, navigation)
|
|
157
|
+
- Use `test.skip(!email, 'ENV_VAR not set — skipping')` guards for credential-dependent tests
|
|
158
|
+
- Auth credentials from env vars via `e2e/helpers/auth.ts` — never hardcode credentials
|
|
159
|
+
- Soft assertions (`expect.soft`) for non-critical checks; hard `expect` for gate conditions
|
|
160
|
+
|
|
161
|
+
E2E spec example structure:
|
|
162
|
+
```ts
|
|
163
|
+
import { test, expect } from '@playwright/test'
|
|
164
|
+
import { signIn, TEST_USERS } from './helpers/auth'
|
|
165
|
+
|
|
166
|
+
test.describe('[Feature] — [scenario]', () => {
|
|
167
|
+
test.beforeEach(async ({ page }) => {
|
|
168
|
+
const { email, password } = TEST_USERS.regular
|
|
169
|
+
test.skip(!email, 'E2E_USER_EMAIL not set — skipping')
|
|
170
|
+
await signIn(page, email, password)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('[behavior description]', async ({ page }) => {
|
|
174
|
+
await page.goto('/dashboard/feature')
|
|
175
|
+
await expect(page.getByRole('heading', { name: /title/i })).toBeVisible()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Create `e2e/helpers/auth.ts` if it doesn't exist (see `/sk:e2e` Playwright Setup Reference).
|
|
181
|
+
|
|
182
|
+
**Run the E2E spec to confirm tests fail or skip** (they should fail until implementation, or skip if env vars aren't set — both are acceptable for the RED phase):
|
|
183
|
+
```bash
|
|
184
|
+
npx playwright test e2e/<feature>.spec.ts --reporter=list
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 8c. Playwright MCP Inspection (optional)
|
|
149
188
|
|
|
150
|
-
**Only if
|
|
189
|
+
**Only if the Playwright MCP plugin is active in the session AND live selectors are needed:**
|
|
151
190
|
|
|
152
191
|
Use the Playwright MCP plugin to inspect live page state for more accurate selectors:
|
|
153
192
|
|
|
154
193
|
1. Navigate to target URL
|
|
155
194
|
2. Capture accessibility snapshot for role-based selectors
|
|
156
195
|
3. Screenshot for visual reference
|
|
157
|
-
4. Optionally run inline assertions for complex interactions
|
|
158
196
|
|
|
159
197
|
### 9. Verify Tests Fail (Red Phase)
|
|
160
198
|
|
|
@@ -170,6 +208,7 @@ Output:
|
|
|
170
208
|
```
|
|
171
209
|
BE tests written: X tests in Y files ([framework])
|
|
172
210
|
FE tests written: X tests in Y files ([framework]) ← omit if no FE stack
|
|
211
|
+
E2E specs written: X tests in Y files (Playwright) ← omit if no playwright.config.ts
|
|
173
212
|
Existing tests updated: X files
|
|
174
213
|
Status: RED (tests fail as expected — ready for implementation)
|
|
175
214
|
```
|