start-vibing-stacks 2.0.0 → 2.0.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/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.2';
|
|
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
|
@@ -12,31 +12,49 @@ Preline is a **semantic token-based design system** built on TailwindCSS. It pro
|
|
|
12
12
|
|
|
13
13
|
## Installation (Laravel + Inertia)
|
|
14
14
|
|
|
15
|
+
### Step 1: Install
|
|
16
|
+
|
|
15
17
|
```bash
|
|
16
|
-
npm install preline
|
|
18
|
+
npm install preline @tailwindcss/forms
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Step 2: CSS Config
|
|
22
|
+
|
|
23
|
+
```css
|
|
24
|
+
/* resources/css/app.css */
|
|
25
|
+
@import "tailwindcss";
|
|
26
|
+
|
|
27
|
+
/* Preline — MUST be in this order */
|
|
28
|
+
@source "./node_modules/preline/dist/*.js"; /* JS component scanning */
|
|
29
|
+
@import "./node_modules/preline/variants.css"; /* CSS variants */
|
|
30
|
+
@plugin "@tailwindcss/forms"; /* Forms plugin */
|
|
31
|
+
@import "./node_modules/preline/themes/theme.css"; /* Base theme */
|
|
17
32
|
```
|
|
18
33
|
|
|
34
|
+
### Step 3: Vite Config
|
|
35
|
+
|
|
19
36
|
```js
|
|
20
37
|
// vite.config.js
|
|
38
|
+
import { defineConfig } from 'vite';
|
|
39
|
+
import laravel from 'laravel-vite-plugin';
|
|
40
|
+
import react from '@vitejs/plugin-react';
|
|
41
|
+
|
|
21
42
|
export default defineConfig({
|
|
22
43
|
plugins: [
|
|
23
|
-
laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'] }),
|
|
44
|
+
laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'], refresh: true }),
|
|
45
|
+
react(),
|
|
24
46
|
],
|
|
25
47
|
});
|
|
26
48
|
```
|
|
27
49
|
|
|
28
|
-
|
|
29
|
-
/* resources/css/app.css */
|
|
30
|
-
@import "tailwindcss";
|
|
31
|
-
@import "preline/themes/theme.css";
|
|
32
|
-
```
|
|
50
|
+
### Step 4: Init Preline in Inertia (MANDATORY)
|
|
33
51
|
|
|
34
52
|
```tsx
|
|
35
|
-
// resources/js/app.tsx
|
|
53
|
+
// resources/js/app.tsx
|
|
36
54
|
import { router } from '@inertiajs/react';
|
|
37
55
|
|
|
56
|
+
// Re-init Preline components after every SPA navigation
|
|
38
57
|
router.on('navigate', () => {
|
|
39
|
-
// Re-init Preline components after SPA navigation
|
|
40
58
|
setTimeout(() => {
|
|
41
59
|
import('preline/preline').then(({ HSStaticMethods }) => {
|
|
42
60
|
HSStaticMethods.autoInit();
|
|
@@ -45,6 +63,77 @@ router.on('navigate', () => {
|
|
|
45
63
|
});
|
|
46
64
|
```
|
|
47
65
|
|
|
66
|
+
**Rule:** Without `HSStaticMethods.autoInit()`, dropdowns, modals, and accordions will NOT work after Inertia navigation.
|
|
67
|
+
|
|
68
|
+
## Templates & Components (840+ free)
|
|
69
|
+
|
|
70
|
+
### Where to Find
|
|
71
|
+
|
|
72
|
+
| Source | URL | What |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| **Docs (components)** | https://preline.co/docs | Buttons, modals, forms, tables, navs |
|
|
75
|
+
| **Examples (blocks)** | https://preline.co/examples.html | 220+ UI blocks (hero, testimonials, pricing, etc.) |
|
|
76
|
+
| **Pro templates** | https://preline.co/pro/templates.html | 21 dashboard/app templates (paid) |
|
|
77
|
+
| **GitHub** | https://github.com/htmlstreamofficial/preline | Source + examples |
|
|
78
|
+
|
|
79
|
+
### How to Use Templates
|
|
80
|
+
|
|
81
|
+
1. Browse https://preline.co/examples.html
|
|
82
|
+
2. Click a block → copy the HTML/JSX
|
|
83
|
+
3. Adapt to React + Inertia:
|
|
84
|
+
- Replace `<a href>` with `<Link href>` (Inertia)
|
|
85
|
+
- Replace `class=` with `className=`
|
|
86
|
+
- Add Preline `data-*` attributes for interactive components
|
|
87
|
+
- Use `usePage().props` for dynamic data
|
|
88
|
+
|
|
89
|
+
### Example: Copy a Hero Block
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// From preline.co/examples.html → Hero sections
|
|
93
|
+
// Adapt HTML to React component:
|
|
94
|
+
export default function HeroSection() {
|
|
95
|
+
return (
|
|
96
|
+
<div className="relative overflow-hidden">
|
|
97
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
|
|
98
|
+
<div className="text-center">
|
|
99
|
+
<h1 className="text-4xl sm:text-6xl font-bold text-foreground">
|
|
100
|
+
Build your next idea
|
|
101
|
+
</h1>
|
|
102
|
+
<p className="mt-4 text-lg text-muted-foreground max-w-2xl mx-auto">
|
|
103
|
+
Preline UI is an open-source set of prebuilt UI components.
|
|
104
|
+
</p>
|
|
105
|
+
<div className="mt-8 flex justify-center gap-3">
|
|
106
|
+
<Link href="/register"
|
|
107
|
+
className="px-6 py-3 bg-primary text-primary-foreground rounded-lg hover:bg-primary-hover font-medium transition-colors">
|
|
108
|
+
Get Started
|
|
109
|
+
</Link>
|
|
110
|
+
<Link href="/docs"
|
|
111
|
+
className="px-6 py-3 bg-layer border border-layer-line text-layer-foreground rounded-lg hover:bg-layer-hover font-medium transition-colors">
|
|
112
|
+
Documentation
|
|
113
|
+
</Link>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Key Component Categories (free)
|
|
123
|
+
|
|
124
|
+
| Category | Count | Examples |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| **Navigation** | 20+ | Navbar, sidebar, breadcrumb, pagination |
|
|
127
|
+
| **Hero** | 11 | Landing page headers |
|
|
128
|
+
| **Cards** | 15+ | Product, blog, profile, pricing |
|
|
129
|
+
| **Forms** | 20+ | Login, register, contact, checkout |
|
|
130
|
+
| **Tables** | 10+ | Sortable, paginated, striped |
|
|
131
|
+
| **Modals** | 8+ | Confirmation, form, full-screen |
|
|
132
|
+
| **Dropdowns** | 10+ | Menu, select, multi-select |
|
|
133
|
+
| **Testimonials** | 10+ | Quotes, carousel, grid |
|
|
134
|
+
| **Pricing** | 8+ | Monthly/yearly toggle, comparison |
|
|
135
|
+
| **Dashboard** | 5+ | Stats, charts, activity feed |
|
|
136
|
+
|
|
48
137
|
## Token Architecture
|
|
49
138
|
|
|
50
139
|
```
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# React UI Patterns — Loading, Errors, Empty States & Forms
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when handling async UI states, forms, or user feedback.**
|
|
4
|
+
|
|
5
|
+
## Core Principles
|
|
6
|
+
|
|
7
|
+
1. **Never show stale UI** — loading only when actually loading
|
|
8
|
+
2. **Always surface errors** — users must KNOW when something fails
|
|
9
|
+
3. **Optimistic updates** — make UI feel instant
|
|
10
|
+
4. **Progressive disclosure** — show content as it becomes available
|
|
11
|
+
5. **Disable during operations** — prevent double-submit
|
|
12
|
+
|
|
13
|
+
## Loading State Decision Tree
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Error?
|
|
17
|
+
→ Yes: Show ErrorState with retry
|
|
18
|
+
→ No ↓
|
|
19
|
+
|
|
20
|
+
Loading AND no data?
|
|
21
|
+
→ Yes: Show Skeleton or Spinner
|
|
22
|
+
→ No ↓
|
|
23
|
+
|
|
24
|
+
Has data?
|
|
25
|
+
→ Yes + items: Render data
|
|
26
|
+
→ Yes + empty: Show EmptyState
|
|
27
|
+
→ No: Show Spinner (fallback)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
// ✅ CORRECT — only loading when no data
|
|
32
|
+
const { data, isLoading, error, refetch } = useQuery(...);
|
|
33
|
+
|
|
34
|
+
if (error) return <ErrorState error={error} onRetry={refetch} />;
|
|
35
|
+
if (isLoading && !data) return <Skeleton />;
|
|
36
|
+
if (!data?.items.length) return <EmptyState />;
|
|
37
|
+
return <ItemList items={data.items} />;
|
|
38
|
+
|
|
39
|
+
// ❌ WRONG — flashes spinner on refetch when cached data exists
|
|
40
|
+
if (isLoading) return <Spinner />;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Skeleton vs Spinner
|
|
44
|
+
|
|
45
|
+
| Use Skeleton | Use Spinner |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Known content shape (cards, tables, lists) | Unknown shape (modals, inline actions) |
|
|
48
|
+
| Initial page load | Button submissions |
|
|
49
|
+
| Content placeholders | Small inline operations |
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// Skeleton component
|
|
53
|
+
function CardSkeleton() {
|
|
54
|
+
return (
|
|
55
|
+
<div className="bg-card border border-card-line rounded-xl p-6 animate-pulse">
|
|
56
|
+
<div className="h-4 w-3/4 bg-muted rounded" />
|
|
57
|
+
<div className="mt-3 h-3 w-1/2 bg-muted rounded" />
|
|
58
|
+
<div className="mt-6 h-10 w-full bg-muted rounded" />
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Skeleton grid
|
|
64
|
+
function ListSkeleton({ count = 6 }: { count?: number }) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
67
|
+
{Array.from({ length: count }, (_, i) => <CardSkeleton key={i} />)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Error Handling Hierarchy
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
Level 1 — Inline error → Field validation (under input)
|
|
77
|
+
Level 2 — Toast notification → Recoverable, user can retry
|
|
78
|
+
Level 3 — Error banner → Page-level, data partially usable
|
|
79
|
+
Level 4 — Full error screen → Unrecoverable, needs user action
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
// Reusable ErrorState
|
|
84
|
+
interface ErrorStateProps {
|
|
85
|
+
error: Error | string;
|
|
86
|
+
onRetry?: () => void;
|
|
87
|
+
title?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ErrorState({ error, onRetry, title }: ErrorStateProps) {
|
|
91
|
+
const message = typeof error === 'string' ? error : error.message;
|
|
92
|
+
return (
|
|
93
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
94
|
+
<div className="h-12 w-12 rounded-full bg-destructive/10 flex items-center justify-center mb-4">
|
|
95
|
+
<AlertCircle className="h-6 w-6 text-destructive" />
|
|
96
|
+
</div>
|
|
97
|
+
<h3 className="text-lg font-semibold text-foreground">{title ?? 'Something went wrong'}</h3>
|
|
98
|
+
<p className="mt-1 text-sm text-muted-foreground max-w-md">{message}</p>
|
|
99
|
+
{onRetry && (
|
|
100
|
+
<button onClick={onRetry}
|
|
101
|
+
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary-hover transition-colors">
|
|
102
|
+
Try Again
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### NEVER Swallow Errors
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// ✅ CORRECT — error surfaced to user
|
|
114
|
+
const mutation = useMutation({
|
|
115
|
+
mutationFn: createItem,
|
|
116
|
+
onSuccess: () => toast.success('Item created!'),
|
|
117
|
+
onError: (error) => {
|
|
118
|
+
console.error('createItem failed:', error);
|
|
119
|
+
toast.error('Failed to create item');
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ❌ WRONG — user sees nothing
|
|
124
|
+
try { await createItem(data); }
|
|
125
|
+
catch (e) { console.log(e); } // Silent failure!
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Empty States
|
|
129
|
+
|
|
130
|
+
**Every list/collection MUST have an empty state.**
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// ✅ With empty state
|
|
134
|
+
function UserList({ users }: { users: User[] }) {
|
|
135
|
+
if (!users.length) {
|
|
136
|
+
return (
|
|
137
|
+
<EmptyState
|
|
138
|
+
icon={<Users className="h-8 w-8" />}
|
|
139
|
+
title="No users yet"
|
|
140
|
+
description="Invite your first team member"
|
|
141
|
+
action={{ label: 'Invite User', onClick: () => router.visit('/users/invite') }}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return <div className="space-y-2">{users.map(u => <UserCard key={u.id} user={u} />)}</div>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Reusable EmptyState
|
|
149
|
+
function EmptyState({ icon, title, description, action }: {
|
|
150
|
+
icon: ReactNode;
|
|
151
|
+
title: string;
|
|
152
|
+
description: string;
|
|
153
|
+
action?: { label: string; onClick: () => void };
|
|
154
|
+
}) {
|
|
155
|
+
return (
|
|
156
|
+
<div className="flex flex-col items-center justify-center py-16 text-center">
|
|
157
|
+
<div className="text-muted-foreground mb-4">{icon}</div>
|
|
158
|
+
<h3 className="text-lg font-semibold text-foreground">{title}</h3>
|
|
159
|
+
<p className="mt-1 text-sm text-muted-foreground max-w-sm">{description}</p>
|
|
160
|
+
{action && (
|
|
161
|
+
<button onClick={action.onClick}
|
|
162
|
+
className="mt-4 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary-hover transition-colors">
|
|
163
|
+
{action.label}
|
|
164
|
+
</button>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Button States
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
// ✅ CORRECT — disabled + loading indicator
|
|
175
|
+
<button
|
|
176
|
+
onClick={handleSubmit}
|
|
177
|
+
disabled={!isValid || isSubmitting}
|
|
178
|
+
className="bg-primary text-primary-foreground px-4 py-2 rounded-lg hover:bg-primary-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
179
|
+
>
|
|
180
|
+
{isSubmitting ? (
|
|
181
|
+
<span className="flex items-center gap-2">
|
|
182
|
+
<Loader className="h-4 w-4 animate-spin" />
|
|
183
|
+
Saving...
|
|
184
|
+
</span>
|
|
185
|
+
) : 'Save'}
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
// ❌ WRONG — user can click multiple times
|
|
189
|
+
<button onClick={handleSubmit}>
|
|
190
|
+
{isSubmitting ? 'Submitting...' : 'Submit'}
|
|
191
|
+
</button>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Form Pattern (Inertia.js)
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { useForm } from '@inertiajs/react';
|
|
198
|
+
|
|
199
|
+
export default function CreateUser() {
|
|
200
|
+
const { data, setData, post, processing, errors, reset } = useForm({
|
|
201
|
+
name: '',
|
|
202
|
+
email: '',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const submit = (e: React.FormEvent) => {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
post('/users', {
|
|
208
|
+
onSuccess: () => {
|
|
209
|
+
toast.success('User created!');
|
|
210
|
+
reset();
|
|
211
|
+
},
|
|
212
|
+
onError: () => toast.error('Failed to create user'),
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<form onSubmit={submit} className="space-y-4">
|
|
218
|
+
<div>
|
|
219
|
+
<label className="block text-sm font-medium text-foreground mb-1">Name</label>
|
|
220
|
+
<input
|
|
221
|
+
value={data.name}
|
|
222
|
+
onChange={e => setData('name', e.target.value)}
|
|
223
|
+
className="w-full h-10 px-3 rounded-md border border-border bg-background text-foreground"
|
|
224
|
+
/>
|
|
225
|
+
{errors.name && <p className="mt-1 text-sm text-destructive">{errors.name}</p>}
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div>
|
|
229
|
+
<label className="block text-sm font-medium text-foreground mb-1">Email</label>
|
|
230
|
+
<input
|
|
231
|
+
type="email"
|
|
232
|
+
value={data.email}
|
|
233
|
+
onChange={e => setData('email', e.target.value)}
|
|
234
|
+
className="w-full h-10 px-3 rounded-md border border-border bg-background text-foreground"
|
|
235
|
+
/>
|
|
236
|
+
{errors.email && <p className="mt-1 text-sm text-destructive">{errors.email}</p>}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<button
|
|
240
|
+
type="submit"
|
|
241
|
+
disabled={processing}
|
|
242
|
+
className="bg-primary text-primary-foreground px-6 py-2 rounded-lg hover:bg-primary-hover disabled:opacity-50 transition-colors"
|
|
243
|
+
>
|
|
244
|
+
{processing ? (
|
|
245
|
+
<span className="flex items-center gap-2">
|
|
246
|
+
<Loader className="h-4 w-4 animate-spin" /> Creating...
|
|
247
|
+
</span>
|
|
248
|
+
) : 'Create User'}
|
|
249
|
+
</button>
|
|
250
|
+
</form>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Optimistic Updates
|
|
256
|
+
|
|
257
|
+
```tsx
|
|
258
|
+
// Show result immediately, rollback on error
|
|
259
|
+
function ToggleFavorite({ item }: { item: Item }) {
|
|
260
|
+
const [optimistic, setOptimistic] = useState(item.isFavorite);
|
|
261
|
+
|
|
262
|
+
const toggle = () => {
|
|
263
|
+
setOptimistic(!optimistic); // Instant UI
|
|
264
|
+
router.post(`/items/${item.id}/favorite`, {}, {
|
|
265
|
+
preserveState: true,
|
|
266
|
+
onError: () => {
|
|
267
|
+
setOptimistic(item.isFavorite); // Rollback
|
|
268
|
+
toast.error('Failed to update');
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<button onClick={toggle} className="text-xl">
|
|
275
|
+
{optimistic ? '❤️' : '🤍'}
|
|
276
|
+
</button>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Checklist — Before Shipping Any UI Component
|
|
282
|
+
|
|
283
|
+
- [ ] Error state handled and shown to user
|
|
284
|
+
- [ ] Loading state only when no data exists (no flash on refetch)
|
|
285
|
+
- [ ] Empty state for every collection/list
|
|
286
|
+
- [ ] Buttons disabled during async operations
|
|
287
|
+
- [ ] Buttons show loading indicator
|
|
288
|
+
- [ ] Form errors shown inline under fields
|
|
289
|
+
- [ ] Mutations have onError with user feedback
|
|
290
|
+
- [ ] Skeleton matches content layout shape
|
|
291
|
+
|
|
292
|
+
## FORBIDDEN
|
|
293
|
+
|
|
294
|
+
1. **`if (loading) return <Spinner />`** — check `loading && !data` instead
|
|
295
|
+
2. **Silent catch** — always toast/display errors to user
|
|
296
|
+
3. **No empty state** — every list needs one
|
|
297
|
+
4. **Clickable button during submit** — always `disabled={processing}`
|
|
298
|
+
5. **Console.log-only errors** — user must see feedback
|