start-vibing-stacks 2.0.2 → 2.0.4
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/dist/ui.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Start Vibing Stacks — Terminal UI
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
const VERSION = '2.0.
|
|
5
|
+
const VERSION = '2.0.4';
|
|
6
6
|
const gradient = (text) => {
|
|
7
7
|
const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
|
|
8
8
|
return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
|
package/package.json
CHANGED
|
@@ -104,10 +104,121 @@ const Heavy = lazy(() => import('./HeavyComponent'));
|
|
|
104
104
|
<Suspense fallback={<Loading />}><Heavy /></Suspense>
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
## React 19 Hooks
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// useActionState — form submission state
|
|
111
|
+
import { useActionState } from 'react';
|
|
112
|
+
|
|
113
|
+
function LoginForm() {
|
|
114
|
+
const [state, formAction, isPending] = useActionState(
|
|
115
|
+
async (prev, formData: FormData) => {
|
|
116
|
+
const result = await login(formData);
|
|
117
|
+
if (result.error) return { error: result.error };
|
|
118
|
+
redirect('/dashboard');
|
|
119
|
+
},
|
|
120
|
+
{ error: null }
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<form action={formAction}>
|
|
125
|
+
<input name="email" type="email" />
|
|
126
|
+
{state.error && <p className="text-destructive">{state.error}</p>}
|
|
127
|
+
<button disabled={isPending}>
|
|
128
|
+
{isPending ? 'Signing in...' : 'Sign In'}
|
|
129
|
+
</button>
|
|
130
|
+
</form>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// useOptimistic — instant UI feedback
|
|
135
|
+
import { useOptimistic } from 'react';
|
|
136
|
+
|
|
137
|
+
function TodoList({ todos }: { todos: Todo[] }) {
|
|
138
|
+
const [optimisticTodos, addOptimistic] = useOptimistic(
|
|
139
|
+
todos,
|
|
140
|
+
(state, newTodo: Todo) => [...state, newTodo]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const addTodo = async (formData: FormData) => {
|
|
144
|
+
const todo = { id: crypto.randomUUID(), title: formData.get('title') as string, done: false };
|
|
145
|
+
addOptimistic(todo); // Instant UI
|
|
146
|
+
await saveTodo(todo); // Server sync
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return <ul>{optimisticTodos.map(t => <li key={t.id}>{t.title}</li>)}</ul>;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## State Management Selection
|
|
154
|
+
|
|
155
|
+
| Complexity | Solution | When |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| Simple local | `useState` | Single component, simple values |
|
|
158
|
+
| Complex local | `useReducer` | Multiple related state transitions |
|
|
159
|
+
| Parent-child | Lift state up | 1-2 levels |
|
|
160
|
+
| Subtree | Context + `useReducer` | Theme, auth, 3+ levels |
|
|
161
|
+
| Server state | React Query / SWR | API data, caching, refetch |
|
|
162
|
+
| Complex global | Zustand | Cross-feature, many consumers |
|
|
163
|
+
|
|
164
|
+
## Error Boundaries
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { Component, type ReactNode } from 'react';
|
|
168
|
+
|
|
169
|
+
class ErrorBoundary extends Component<
|
|
170
|
+
{ children: ReactNode; fallback?: ReactNode },
|
|
171
|
+
{ hasError: boolean; error?: Error }
|
|
172
|
+
> {
|
|
173
|
+
state = { hasError: false, error: undefined as Error | undefined };
|
|
174
|
+
|
|
175
|
+
static getDerivedStateFromError(error: Error) {
|
|
176
|
+
return { hasError: true, error };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
180
|
+
console.error('ErrorBoundary caught:', error, info);
|
|
181
|
+
// Send to Sentry/logging
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
render() {
|
|
185
|
+
if (this.state.hasError) {
|
|
186
|
+
return this.props.fallback ?? (
|
|
187
|
+
<div className="p-8 text-center">
|
|
188
|
+
<h2 className="text-lg font-semibold text-foreground">Something went wrong</h2>
|
|
189
|
+
<button onClick={() => this.setState({ hasError: false })}
|
|
190
|
+
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg">
|
|
191
|
+
Try Again
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return this.props.children;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Usage: wrap routes/features
|
|
201
|
+
<ErrorBoundary fallback={<ErrorPage />}>
|
|
202
|
+
<DashboardFeature />
|
|
203
|
+
</ErrorBoundary>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Component Types
|
|
207
|
+
|
|
208
|
+
| Type | Use | Example |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| **Server** (RSC) | Data fetching, static content | Page-level components |
|
|
211
|
+
| **Client** (`'use client'`) | Interactivity, hooks, browser APIs | Forms, modals, dropdowns |
|
|
212
|
+
| **Presentational** | Pure display, props only | `<Badge>`, `<Avatar>` |
|
|
213
|
+
| **Container** | Logic + state, renders presentational | `<UserListContainer>` |
|
|
214
|
+
|
|
107
215
|
## FORBIDDEN
|
|
108
216
|
|
|
109
|
-
1. **Class components** — function components only
|
|
217
|
+
1. **Class components** — function components only (except ErrorBoundary)
|
|
110
218
|
2. **Prop drilling** — use context or composition
|
|
111
219
|
3. **Inline objects/functions in JSX** — causes re-renders
|
|
112
220
|
4. **useEffect for derived state** — use useMemo
|
|
113
221
|
5. **Mutating state directly** — always setState/dispatch
|
|
222
|
+
6. **Index as key** — use stable unique ID (`uuid`, `id`)
|
|
223
|
+
7. **Premature optimization** — profile first with React DevTools
|
|
224
|
+
8. **useEffect for everything** — prefer server components for data
|
|
@@ -49,27 +49,139 @@ export default function Dashboard() {
|
|
|
49
49
|
|
|
50
50
|
**Rule:** Never leave raw `console.log`. Always use controlled debug pattern.
|
|
51
51
|
|
|
52
|
-
## TailwindCSS Class Organization
|
|
52
|
+
## TailwindCSS Class Organization (CONST Pattern)
|
|
53
|
+
|
|
54
|
+
**MANDATORY:** Define all CSS classes as constants at the TOP of the file, before the component.
|
|
55
|
+
|
|
56
|
+
### Why
|
|
57
|
+
|
|
58
|
+
1. **No re-renders** — string constants have stable references (no new object per render)
|
|
59
|
+
2. **Single source of truth** — change style once, updates everywhere
|
|
60
|
+
3. **Clean JSX** — readable templates, no class soup
|
|
61
|
+
4. **Prevents React state warnings** — no inline objects/strings changing reference
|
|
62
|
+
5. **Easy theming** — swap tokens in one place
|
|
63
|
+
|
|
64
|
+
### Pattern
|
|
53
65
|
|
|
54
66
|
```tsx
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
};
|
|
67
|
+
// ═══════════════════════════════════════════
|
|
68
|
+
// 1. TRANSLATIONS (before hooks)
|
|
69
|
+
// ═══════════════════════════════════════════
|
|
70
|
+
const LABELS = {
|
|
71
|
+
title: __('dashboard.title'),
|
|
72
|
+
save: __('common.save'),
|
|
73
|
+
} as const;
|
|
62
74
|
|
|
75
|
+
// ═══════════════════════════════════════════
|
|
76
|
+
// 2. STYLES (semantic tokens, not raw colors)
|
|
77
|
+
// ═══════════════════════════════════════════
|
|
78
|
+
const STYLES = {
|
|
79
|
+
// Layout
|
|
80
|
+
page: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8',
|
|
81
|
+
section: 'space-y-6',
|
|
82
|
+
grid: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4',
|
|
83
|
+
|
|
84
|
+
// Cards
|
|
85
|
+
card: 'bg-card border border-card-line rounded-xl p-6 hover:shadow-md transition-shadow',
|
|
86
|
+
cardHeader: 'flex items-center justify-between border-b border-card-divider pb-4 mb-4',
|
|
87
|
+
cardTitle: 'text-lg font-semibold text-foreground',
|
|
88
|
+
cardDescription: 'text-sm text-muted-foreground mt-1',
|
|
89
|
+
|
|
90
|
+
// Table
|
|
91
|
+
table: 'w-full text-sm',
|
|
92
|
+
tableHeader: 'text-left text-muted-foreground font-medium border-b border-border',
|
|
93
|
+
tableRow: 'border-b border-border hover:bg-muted/50 transition-colors',
|
|
94
|
+
tableCell: 'px-4 py-3 text-foreground',
|
|
95
|
+
|
|
96
|
+
// Buttons
|
|
97
|
+
btnPrimary: 'px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary-hover font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors',
|
|
98
|
+
btnSecondary: 'px-4 py-2 bg-layer border border-layer-line text-layer-foreground rounded-lg hover:bg-layer-hover font-medium transition-colors',
|
|
99
|
+
btnDestructive: 'px-4 py-2 bg-destructive text-destructive-foreground rounded-lg hover:bg-destructive-hover font-medium transition-colors',
|
|
100
|
+
btnGhost: 'px-4 py-2 text-muted-foreground hover:bg-muted rounded-lg transition-colors',
|
|
101
|
+
|
|
102
|
+
// Forms
|
|
103
|
+
input: 'w-full h-10 px-3 rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent transition-colors',
|
|
104
|
+
label: 'block text-sm font-medium text-foreground mb-1',
|
|
105
|
+
fieldError: 'mt-1 text-sm text-destructive',
|
|
106
|
+
|
|
107
|
+
// Status badges
|
|
108
|
+
badgeSuccess: 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
|
109
|
+
badgeWarning: 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
|
|
110
|
+
badgeDanger: 'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
|
|
111
|
+
|
|
112
|
+
// Typography
|
|
113
|
+
heading: 'text-2xl font-bold text-foreground',
|
|
114
|
+
subheading: 'text-lg font-semibold text-foreground',
|
|
115
|
+
body: 'text-sm text-foreground',
|
|
116
|
+
muted: 'text-sm text-muted-foreground',
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
119
|
+
// ═══════════════════════════════════════════
|
|
120
|
+
// 3. COMPONENT
|
|
121
|
+
// ═══════════════════════════════════════════
|
|
63
122
|
export default function Dashboard() {
|
|
123
|
+
const [data, setData] = useState(null);
|
|
124
|
+
|
|
64
125
|
return (
|
|
65
|
-
<div className={STYLES.
|
|
66
|
-
<h1 className={STYLES.
|
|
126
|
+
<div className={STYLES.page}>
|
|
127
|
+
<h1 className={STYLES.heading}>{LABELS.title}</h1>
|
|
128
|
+
<div className={STYLES.grid}>
|
|
129
|
+
<div className={STYLES.card}>
|
|
130
|
+
<h2 className={STYLES.cardTitle}>Stats</h2>
|
|
131
|
+
<p className={STYLES.cardDescription}>Overview</p>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
67
134
|
</div>
|
|
68
135
|
);
|
|
69
136
|
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Composing Styles
|
|
70
140
|
|
|
71
|
-
|
|
72
|
-
|
|
141
|
+
```tsx
|
|
142
|
+
// ✅ Combine with template literal when conditional
|
|
143
|
+
<tr className={`${STYLES.tableRow} ${isSelected ? 'bg-primary/5' : ''}`}>
|
|
144
|
+
|
|
145
|
+
// ✅ clsx/cn for complex conditions
|
|
146
|
+
import { cn } from '@/lib/utils';
|
|
147
|
+
<button className={cn(STYLES.btnPrimary, isFullWidth && 'w-full', className)}>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Rules
|
|
151
|
+
|
|
152
|
+
1. **CONST at top** — before hooks, before component
|
|
153
|
+
2. **`as const`** — TypeScript ensures immutability
|
|
154
|
+
3. **Semantic tokens** — `bg-card` not `bg-white`, `text-foreground` not `text-gray-900`
|
|
155
|
+
4. **No inline class strings > 3 utilities** — extract to STYLES
|
|
156
|
+
5. **Shared styles** — create a `styles.ts` file for cross-component constants
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// resources/js/styles.ts — shared across components
|
|
160
|
+
export const SHARED_STYLES = {
|
|
161
|
+
page: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8',
|
|
162
|
+
btnPrimary: 'px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary-hover ...',
|
|
163
|
+
input: 'w-full h-10 px-3 rounded-md border border-border bg-background ...',
|
|
164
|
+
} as const;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### ❌ FORBIDDEN
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// ❌ Inline class soup — unreadable, unstable reference
|
|
171
|
+
<div className="flex flex-col gap-4 p-6 bg-white rounded-lg shadow-sm">
|
|
172
|
+
|
|
173
|
+
// ❌ Raw colors instead of tokens
|
|
174
|
+
const STYLES = { card: 'bg-white text-gray-900' }; // ❌ Breaks dark mode
|
|
175
|
+
|
|
176
|
+
// ❌ Dynamic class construction (breaks Tailwind purge)
|
|
177
|
+
const color = 'blue';
|
|
178
|
+
<div className={`bg-${color}-500`}> // ❌ Purged!
|
|
179
|
+
|
|
180
|
+
// ❌ Styles inside component (new object every render)
|
|
181
|
+
export default function Bad() {
|
|
182
|
+
const styles = { card: 'bg-card p-4' }; // ❌ Inside = new ref every render
|
|
183
|
+
return <div className={styles.card} />;
|
|
184
|
+
}
|
|
73
185
|
```
|
|
74
186
|
|
|
75
187
|
## SVG Icons
|