@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,189 @@
|
|
|
1
|
+
# Next.js + Tailwind — Stack Reference
|
|
2
|
+
|
|
3
|
+
## Scaffold
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx create-next-app@latest {project-name} --typescript --tailwind --eslint --app --src-dir --no-import-alias
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Then `cd {project-name} && npm install`.
|
|
10
|
+
|
|
11
|
+
## Directory Structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
{project-name}/
|
|
15
|
+
├── src/
|
|
16
|
+
│ ├── app/
|
|
17
|
+
│ │ ├── layout.tsx ← root layout (fonts, global styles)
|
|
18
|
+
│ │ ├── page.tsx ← landing page
|
|
19
|
+
│ │ ├── globals.css ← Tailwind directives + custom CSS
|
|
20
|
+
│ │ ├── api/
|
|
21
|
+
│ │ │ └── waitlist/
|
|
22
|
+
│ │ │ └── route.ts ← waitlist API handler
|
|
23
|
+
│ │ ├── dashboard/
|
|
24
|
+
│ │ │ └── page.tsx ← dashboard page
|
|
25
|
+
│ │ ├── {feature-1}/
|
|
26
|
+
│ │ │ └── page.tsx
|
|
27
|
+
│ │ ├── {feature-2}/
|
|
28
|
+
│ │ │ └── page.tsx
|
|
29
|
+
│ │ └── settings/
|
|
30
|
+
│ │ └── page.tsx
|
|
31
|
+
│ └── components/
|
|
32
|
+
│ ├── landing/
|
|
33
|
+
│ │ ├── Navbar.tsx
|
|
34
|
+
│ │ ├── Hero.tsx
|
|
35
|
+
│ │ ├── Features.tsx
|
|
36
|
+
│ │ ├── HowItWorks.tsx
|
|
37
|
+
│ │ ├── Pricing.tsx
|
|
38
|
+
│ │ ├── Testimonials.tsx
|
|
39
|
+
│ │ ├── WaitlistForm.tsx
|
|
40
|
+
│ │ └── Footer.tsx
|
|
41
|
+
│ ├── app/
|
|
42
|
+
│ │ ├── Sidebar.tsx
|
|
43
|
+
│ │ ├── DashboardCards.tsx
|
|
44
|
+
│ │ └── {feature components}
|
|
45
|
+
│ └── ui/
|
|
46
|
+
│ ├── Button.tsx
|
|
47
|
+
│ ├── Input.tsx
|
|
48
|
+
│ ├── Card.tsx
|
|
49
|
+
│ ├── Modal.tsx
|
|
50
|
+
│ └── Toast.tsx
|
|
51
|
+
├── public/
|
|
52
|
+
│ └── {static assets}
|
|
53
|
+
├── waitlist.json ← email storage (auto-created by API)
|
|
54
|
+
├── tailwind.config.ts
|
|
55
|
+
├── next.config.ts
|
|
56
|
+
└── package.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Root Layout Pattern
|
|
60
|
+
|
|
61
|
+
`src/app/layout.tsx`:
|
|
62
|
+
- Import Google Fonts via `next/font/google`.
|
|
63
|
+
- Apply font CSS variables to `<html>` element.
|
|
64
|
+
- Include global nav only for app pages (not landing page — it has its own navbar).
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { {DisplayFont}, {BodyFont} } from 'next/font/google'
|
|
68
|
+
|
|
69
|
+
const display = {DisplayFont}({ subsets: ['latin'], variable: '--font-display' })
|
|
70
|
+
const body = {BodyFont}({ subsets: ['latin'], variable: '--font-body' })
|
|
71
|
+
|
|
72
|
+
export default function RootLayout({ children }) {
|
|
73
|
+
return (
|
|
74
|
+
<html className={`${display.variable} ${body.variable}`}>
|
|
75
|
+
<body className="font-body antialiased">{children}</body>
|
|
76
|
+
</html>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tailwind Config
|
|
82
|
+
|
|
83
|
+
`tailwind.config.ts` — extend with custom palette and fonts:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export default {
|
|
87
|
+
theme: {
|
|
88
|
+
extend: {
|
|
89
|
+
colors: {
|
|
90
|
+
bg: 'var(--color-bg)',
|
|
91
|
+
fg: 'var(--color-fg)',
|
|
92
|
+
accent: 'var(--color-accent)',
|
|
93
|
+
muted: 'var(--color-muted)',
|
|
94
|
+
// add more as needed
|
|
95
|
+
},
|
|
96
|
+
fontFamily: {
|
|
97
|
+
display: ['var(--font-display)', 'serif'],
|
|
98
|
+
body: ['var(--font-body)', 'sans-serif'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Define CSS variables in `globals.css`:
|
|
106
|
+
|
|
107
|
+
```css
|
|
108
|
+
@tailwind base;
|
|
109
|
+
@tailwind components;
|
|
110
|
+
@tailwind utilities;
|
|
111
|
+
|
|
112
|
+
:root {
|
|
113
|
+
--color-bg: #xxxxxx;
|
|
114
|
+
--color-fg: #xxxxxx;
|
|
115
|
+
--color-accent: #xxxxxx;
|
|
116
|
+
--color-muted: #xxxxxx;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Waitlist API Route
|
|
121
|
+
|
|
122
|
+
`src/app/api/waitlist/route.ts`:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { NextResponse } from 'next/server'
|
|
126
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
127
|
+
import { join } from 'path'
|
|
128
|
+
|
|
129
|
+
const WAITLIST_PATH = join(process.cwd(), 'waitlist.json')
|
|
130
|
+
|
|
131
|
+
export async function POST(request: Request) {
|
|
132
|
+
const { email } = await request.json()
|
|
133
|
+
|
|
134
|
+
// Validate
|
|
135
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
136
|
+
return NextResponse.json({ success: false, message: 'Please enter a valid email.' }, { status: 400 })
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Read or create
|
|
140
|
+
let data = { entries: [] as Array<{ email: string; timestamp: string; source: string }> }
|
|
141
|
+
try {
|
|
142
|
+
const raw = await readFile(WAITLIST_PATH, 'utf-8')
|
|
143
|
+
data = JSON.parse(raw)
|
|
144
|
+
} catch {
|
|
145
|
+
// File doesn't exist yet — use empty default
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check duplicate
|
|
149
|
+
if (data.entries.some(e => e.email === email)) {
|
|
150
|
+
return NextResponse.json({ success: true, message: "You're already on the list!" })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Append
|
|
154
|
+
data.entries.push({ email, timestamp: new Date().toISOString(), source: 'landing-page' })
|
|
155
|
+
await writeFile(WAITLIST_PATH, JSON.stringify(data, null, 2))
|
|
156
|
+
|
|
157
|
+
return NextResponse.json({ success: true, message: "You're on the list!" })
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Component Patterns
|
|
162
|
+
|
|
163
|
+
- Landing page components are **server components** by default (no `"use client"`).
|
|
164
|
+
- Interactive components (WaitlistForm, modals, toasts) need `"use client"` directive.
|
|
165
|
+
- Use `useState` for local UI state (form values, modal open/close).
|
|
166
|
+
- Navigation between app pages: use `<Link>` from `next/link`.
|
|
167
|
+
- App pages can share a layout: `src/app/(app)/layout.tsx` with sidebar.
|
|
168
|
+
|
|
169
|
+
### App Layout (grouped routes)
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
src/app/
|
|
173
|
+
├── page.tsx ← landing page (no app layout)
|
|
174
|
+
├── (app)/
|
|
175
|
+
│ ├── layout.tsx ← shared sidebar + header
|
|
176
|
+
│ ├── dashboard/page.tsx
|
|
177
|
+
│ ├── {feature-1}/page.tsx
|
|
178
|
+
│ ├── {feature-2}/page.tsx
|
|
179
|
+
│ └── settings/page.tsx
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`(app)/layout.tsx` wraps app pages with sidebar navigation without affecting the landing page.
|
|
183
|
+
|
|
184
|
+
## Dev Server
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npm run dev
|
|
188
|
+
# Runs on http://localhost:3000
|
|
189
|
+
```
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Nuxt + Tailwind — Stack Reference
|
|
2
|
+
|
|
3
|
+
## Scaffold
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx nuxi@latest init {project-name}
|
|
7
|
+
cd {project-name}
|
|
8
|
+
npm install
|
|
9
|
+
npx nuxi module add @nuxtjs/tailwindcss
|
|
10
|
+
npx nuxi module add @nuxtjs/google-fonts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Directory Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{project-name}/
|
|
17
|
+
├── pages/
|
|
18
|
+
│ ├── index.vue ← landing page
|
|
19
|
+
│ ├── dashboard.vue ← dashboard
|
|
20
|
+
│ ├── {feature-1}.vue
|
|
21
|
+
│ ├── {feature-2}.vue
|
|
22
|
+
│ └── settings.vue
|
|
23
|
+
├── components/
|
|
24
|
+
│ ├── landing/
|
|
25
|
+
│ │ ├── Navbar.vue
|
|
26
|
+
│ │ ├── Hero.vue
|
|
27
|
+
│ │ ├── Features.vue
|
|
28
|
+
│ │ ├── HowItWorks.vue
|
|
29
|
+
│ │ ├── Pricing.vue
|
|
30
|
+
│ │ ├── Testimonials.vue
|
|
31
|
+
│ │ ├── WaitlistForm.vue
|
|
32
|
+
│ │ └── Footer.vue
|
|
33
|
+
│ ├── app/
|
|
34
|
+
│ │ ├── Sidebar.vue
|
|
35
|
+
│ │ ├── DashboardCards.vue
|
|
36
|
+
│ │ └── {feature components}
|
|
37
|
+
│ └── ui/
|
|
38
|
+
│ ├── UButton.vue
|
|
39
|
+
│ ├── UInput.vue
|
|
40
|
+
│ ├── UCard.vue
|
|
41
|
+
│ ├── UModal.vue
|
|
42
|
+
│ └── UToast.vue
|
|
43
|
+
├── layouts/
|
|
44
|
+
│ ├── default.vue ← app layout (sidebar + header)
|
|
45
|
+
│ └── landing.vue ← landing page layout (no sidebar)
|
|
46
|
+
├── server/
|
|
47
|
+
│ └── api/
|
|
48
|
+
│ └── waitlist.post.ts ← waitlist API handler
|
|
49
|
+
├── public/
|
|
50
|
+
│ └── {static assets}
|
|
51
|
+
├── waitlist.json ← email storage (auto-created by API)
|
|
52
|
+
├── tailwind.config.ts
|
|
53
|
+
├── nuxt.config.ts
|
|
54
|
+
└── package.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Nuxt Config
|
|
58
|
+
|
|
59
|
+
`nuxt.config.ts`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
export default defineNuxtConfig({
|
|
63
|
+
modules: [
|
|
64
|
+
'@nuxtjs/tailwindcss',
|
|
65
|
+
'@nuxtjs/google-fonts',
|
|
66
|
+
],
|
|
67
|
+
googleFonts: {
|
|
68
|
+
families: {
|
|
69
|
+
'{DisplayFont}': [400, 600, 700, 800],
|
|
70
|
+
'{BodyFont}': [400, 500, 600],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
app: {
|
|
74
|
+
head: {
|
|
75
|
+
title: '{Product Name}',
|
|
76
|
+
meta: [
|
|
77
|
+
{ name: 'description', content: '{product description}' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Tailwind Config
|
|
85
|
+
|
|
86
|
+
`tailwind.config.ts`:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export default {
|
|
90
|
+
theme: {
|
|
91
|
+
extend: {
|
|
92
|
+
colors: {
|
|
93
|
+
bg: 'var(--color-bg)',
|
|
94
|
+
fg: 'var(--color-fg)',
|
|
95
|
+
accent: 'var(--color-accent)',
|
|
96
|
+
muted: 'var(--color-muted)',
|
|
97
|
+
},
|
|
98
|
+
fontFamily: {
|
|
99
|
+
display: ['{DisplayFont}', 'serif'],
|
|
100
|
+
body: ['{BodyFont}', 'sans-serif'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
CSS variables in `assets/css/main.css` (referenced in nuxt.config):
|
|
108
|
+
|
|
109
|
+
```css
|
|
110
|
+
:root {
|
|
111
|
+
--color-bg: #xxxxxx;
|
|
112
|
+
--color-fg: #xxxxxx;
|
|
113
|
+
--color-accent: #xxxxxx;
|
|
114
|
+
--color-muted: #xxxxxx;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
body {
|
|
118
|
+
font-family: '{BodyFont}', sans-serif;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Layouts
|
|
123
|
+
|
|
124
|
+
### Landing Layout (`layouts/landing.vue`)
|
|
125
|
+
|
|
126
|
+
```vue
|
|
127
|
+
<template>
|
|
128
|
+
<div class="min-h-screen bg-bg text-fg">
|
|
129
|
+
<slot />
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Landing page uses this layout via `definePageMeta({ layout: 'landing' })`.
|
|
135
|
+
|
|
136
|
+
### App Layout (`layouts/default.vue`)
|
|
137
|
+
|
|
138
|
+
```vue
|
|
139
|
+
<template>
|
|
140
|
+
<div class="flex min-h-screen bg-bg text-fg">
|
|
141
|
+
<AppSidebar />
|
|
142
|
+
<main class="flex-1 p-6 lg:p-8">
|
|
143
|
+
<slot />
|
|
144
|
+
</main>
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
All app pages use this layout by default.
|
|
150
|
+
|
|
151
|
+
## Waitlist API Route
|
|
152
|
+
|
|
153
|
+
`server/api/waitlist.post.ts`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { readFile, writeFile } from 'fs/promises'
|
|
157
|
+
import { join } from 'path'
|
|
158
|
+
|
|
159
|
+
const WAITLIST_PATH = join(process.cwd(), 'waitlist.json')
|
|
160
|
+
|
|
161
|
+
export default defineEventHandler(async (event) => {
|
|
162
|
+
const { email } = await readBody(event)
|
|
163
|
+
|
|
164
|
+
// Validate
|
|
165
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
166
|
+
throw createError({ statusCode: 400, message: 'Please enter a valid email.' })
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Read or create
|
|
170
|
+
let data: { entries: Array<{ email: string; timestamp: string; source: string }> } = { entries: [] }
|
|
171
|
+
try {
|
|
172
|
+
const raw = await readFile(WAITLIST_PATH, 'utf-8')
|
|
173
|
+
data = JSON.parse(raw)
|
|
174
|
+
} catch {
|
|
175
|
+
// File doesn't exist yet
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check duplicate
|
|
179
|
+
if (data.entries.some(e => e.email === email)) {
|
|
180
|
+
return { success: true, message: "You're already on the list!" }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Append
|
|
184
|
+
data.entries.push({ email, timestamp: new Date().toISOString(), source: 'landing-page' })
|
|
185
|
+
await writeFile(WAITLIST_PATH, JSON.stringify(data, null, 2))
|
|
186
|
+
|
|
187
|
+
return { success: true, message: "You're on the list!" }
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Component Patterns
|
|
192
|
+
|
|
193
|
+
- Use Vue 3 Composition API with `<script setup lang="ts">`.
|
|
194
|
+
- Components are auto-imported by Nuxt (no manual imports needed).
|
|
195
|
+
- Use `ref()` and `reactive()` for state management.
|
|
196
|
+
- Navigation: `<NuxtLink to="/dashboard">`.
|
|
197
|
+
- Pages set layout via `definePageMeta({ layout: 'landing' })`.
|
|
198
|
+
|
|
199
|
+
### Page Example
|
|
200
|
+
|
|
201
|
+
```vue
|
|
202
|
+
<script setup lang="ts">
|
|
203
|
+
definePageMeta({ layout: 'landing' })
|
|
204
|
+
</script>
|
|
205
|
+
|
|
206
|
+
<template>
|
|
207
|
+
<div>
|
|
208
|
+
<LandingNavbar />
|
|
209
|
+
<LandingHero />
|
|
210
|
+
<LandingFeatures />
|
|
211
|
+
<LandingHowItWorks />
|
|
212
|
+
<LandingPricing />
|
|
213
|
+
<LandingTestimonials />
|
|
214
|
+
<LandingWaitlistForm />
|
|
215
|
+
<LandingFooter />
|
|
216
|
+
</div>
|
|
217
|
+
</template>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### WaitlistForm Component
|
|
221
|
+
|
|
222
|
+
```vue
|
|
223
|
+
<script setup lang="ts">
|
|
224
|
+
const email = ref('')
|
|
225
|
+
const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
226
|
+
const message = ref('')
|
|
227
|
+
|
|
228
|
+
async function submit() {
|
|
229
|
+
status.value = 'loading'
|
|
230
|
+
try {
|
|
231
|
+
const res = await $fetch('/api/waitlist', {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
body: { email: email.value },
|
|
234
|
+
})
|
|
235
|
+
status.value = 'success'
|
|
236
|
+
message.value = res.message
|
|
237
|
+
} catch (err: any) {
|
|
238
|
+
status.value = 'error'
|
|
239
|
+
message.value = err.data?.message || 'Something went wrong.'
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
</script>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Dev Server
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npm run dev
|
|
249
|
+
# Runs on http://localhost:3000
|
|
250
|
+
```
|