start-vibing-stacks 1.9.1 → 2.0.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/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 = '
|
|
5
|
+
const VERSION = '2.0.0';
|
|
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
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Preline UI — Component & Theme System for TailwindCSS 4
|
|
2
|
+
|
|
3
|
+
**ALWAYS invoke when using Preline components, creating themes, or customizing design tokens.**
|
|
4
|
+
|
|
5
|
+
## What is Preline
|
|
6
|
+
|
|
7
|
+
Preline is a **semantic token-based design system** built on TailwindCSS. It provides:
|
|
8
|
+
- 220+ CSS tokens for full UI consistency
|
|
9
|
+
- Pre-built components (navbar, sidebar, card, dropdown, overlay, etc.)
|
|
10
|
+
- Theme generator for custom color schemes
|
|
11
|
+
- Light + dark mode via `data-theme` + `.dark`
|
|
12
|
+
|
|
13
|
+
## Installation (Laravel + Inertia)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install preline
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
// vite.config.js
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
plugins: [
|
|
23
|
+
laravel({ input: ['resources/css/app.css', 'resources/js/app.tsx'] }),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```css
|
|
29
|
+
/* resources/css/app.css */
|
|
30
|
+
@import "tailwindcss";
|
|
31
|
+
@import "preline/themes/theme.css";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// resources/js/app.tsx — init Preline after Inertia navigation
|
|
36
|
+
import { router } from '@inertiajs/react';
|
|
37
|
+
|
|
38
|
+
router.on('navigate', () => {
|
|
39
|
+
// Re-init Preline components after SPA navigation
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
import('preline/preline').then(({ HSStaticMethods }) => {
|
|
42
|
+
HSStaticMethods.autoInit();
|
|
43
|
+
});
|
|
44
|
+
}, 100);
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Token Architecture
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Layer 1 — Tailwind primitives: var(--color-blue-600)
|
|
52
|
+
Layer 2 — Preline semantic: --primary: var(--color-blue-600)
|
|
53
|
+
Layer 3 — Component tokens: --navbar-nav-hover: var(--color-gray-100)
|
|
54
|
+
Layer 4 — Usage in HTML: className="bg-primary text-primary-foreground"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Core Token Groups
|
|
58
|
+
|
|
59
|
+
| Group | Example Tokens | Purpose |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| **Background** | `--background`, `--background-1`, `--background-2` | App surfaces |
|
|
62
|
+
| **Foreground** | `--foreground`, `--foreground-inverse` | Text colors |
|
|
63
|
+
| **Primary** | `--primary`, `--primary-hover`, `--primary-foreground`, `--primary-50`→`950` | Brand/action color |
|
|
64
|
+
| **Secondary** | `--secondary`, `--secondary-hover`, `--secondary-foreground` | Secondary emphasis |
|
|
65
|
+
| **Muted** | `--muted`, `--muted-foreground`, `--muted-foreground-1`, `--muted-foreground-2` | Subdued elements |
|
|
66
|
+
| **Destructive** | `--destructive`, `--destructive-hover`, `--destructive-foreground` | Danger actions |
|
|
67
|
+
| **Border** | `--border`, `--border-line-1`→`8` | Border scale (light→dark) |
|
|
68
|
+
| **Surface** | `--surface`, `--surface-1`→`5`, `--surface-foreground` | Elevated layers |
|
|
69
|
+
| **Layer** | `--layer`, `--layer-hover`, `--layer-foreground` | Stacked elements |
|
|
70
|
+
|
|
71
|
+
### Component Token Groups
|
|
72
|
+
|
|
73
|
+
| Component | Tokens | Notes |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| **Navbar** | `--navbar`, `--navbar-line`, `--navbar-nav-*` | 3 variants (base, `-1`, `-2`) |
|
|
76
|
+
| **Sidebar** | `--sidebar`, `--sidebar-line`, `--sidebar-nav-*` | 3 variants (base, `-1`, `-2`) |
|
|
77
|
+
| **Card** | `--card`, `--card-line`, `--card-divider`, `--card-header`, `--card-footer` | |
|
|
78
|
+
| **Dropdown** | `--dropdown`, `--dropdown-item-*` | hover, focus, active states |
|
|
79
|
+
| **Select** | `--select`, `--select-item-*` | Same state pattern |
|
|
80
|
+
| **Overlay** | `--overlay`, `--overlay-line`, `--overlay-header`, `--overlay-footer` | Modals |
|
|
81
|
+
| **Tooltip** | `--tooltip`, `--tooltip-foreground`, `--tooltip-line` | |
|
|
82
|
+
| **Popover** | `--popover`, `--popover-line` | |
|
|
83
|
+
| **Scrollbar** | `--scrollbar-track`, `--scrollbar-thumb` | + inverse variants |
|
|
84
|
+
|
|
85
|
+
## Creating Custom Themes
|
|
86
|
+
|
|
87
|
+
### Theme File Structure (MUST follow order)
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
/* 1. Import base theme */
|
|
91
|
+
@import "./theme.css";
|
|
92
|
+
|
|
93
|
+
/* 2. Theme scoping block — custom palettes ONLY */
|
|
94
|
+
@theme theme-brand inline {
|
|
95
|
+
--color-brand-50: oklch(98% 0.003 250);
|
|
96
|
+
--color-brand-100: oklch(95% 0.01 250);
|
|
97
|
+
--color-brand-200: oklch(88% 0.03 250);
|
|
98
|
+
--color-brand-300: oklch(78% 0.06 250);
|
|
99
|
+
--color-brand-400: oklch(68% 0.10 250);
|
|
100
|
+
--color-brand-500: oklch(58% 0.14 250);
|
|
101
|
+
--color-brand-600: oklch(50% 0.15 250);
|
|
102
|
+
--color-brand-700: oklch(42% 0.13 250);
|
|
103
|
+
--color-brand-800: oklch(35% 0.10 250);
|
|
104
|
+
--color-brand-900: oklch(28% 0.08 250);
|
|
105
|
+
--color-brand-950: oklch(20% 0.05 250);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* 3. Light mode — semantic token overrides */
|
|
109
|
+
:root[data-theme="theme-brand"],
|
|
110
|
+
[data-theme="theme-brand"] {
|
|
111
|
+
--background: var(--color-white);
|
|
112
|
+
--foreground: var(--color-gray-800);
|
|
113
|
+
|
|
114
|
+
--primary: var(--color-brand-600);
|
|
115
|
+
--primary-foreground: var(--color-white);
|
|
116
|
+
--primary-hover: var(--color-brand-700);
|
|
117
|
+
--primary-focus: var(--color-brand-700);
|
|
118
|
+
--primary-active: var(--color-brand-700);
|
|
119
|
+
|
|
120
|
+
/* ... all 220+ tokens as needed */
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* 4. Dark mode */
|
|
124
|
+
[data-theme="theme-brand"].dark {
|
|
125
|
+
--background: var(--color-neutral-800);
|
|
126
|
+
--foreground: var(--color-neutral-200);
|
|
127
|
+
|
|
128
|
+
--primary: var(--color-brand-500);
|
|
129
|
+
--primary-foreground: var(--color-white);
|
|
130
|
+
--primary-hover: var(--color-brand-600);
|
|
131
|
+
|
|
132
|
+
/* ... dark overrides */
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Activation
|
|
137
|
+
|
|
138
|
+
```html
|
|
139
|
+
<html data-theme="theme-brand">
|
|
140
|
+
<!-- Or with dark mode: -->
|
|
141
|
+
<html data-theme="theme-brand" class="dark">
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
// React theme switcher
|
|
146
|
+
function ThemeToggle() {
|
|
147
|
+
const [dark, setDark] = useState(false);
|
|
148
|
+
return (
|
|
149
|
+
<button onClick={() => {
|
|
150
|
+
document.documentElement.classList.toggle('dark');
|
|
151
|
+
setDark(!dark);
|
|
152
|
+
}}>
|
|
153
|
+
{dark ? '☀️' : '🌙'}
|
|
154
|
+
</button>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Using Components with React (Inertia)
|
|
160
|
+
|
|
161
|
+
### Navbar
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
<nav className="bg-navbar border-b border-navbar-line">
|
|
165
|
+
<div className="max-w-7xl mx-auto px-4 flex items-center justify-between h-16">
|
|
166
|
+
<Link href="/" className="text-foreground font-bold text-lg">Brand</Link>
|
|
167
|
+
<div className="flex items-center gap-1">
|
|
168
|
+
<Link href="/dashboard"
|
|
169
|
+
className="px-3 py-2 rounded-lg text-navbar-nav-foreground hover:bg-navbar-nav-hover transition-colors">
|
|
170
|
+
Dashboard
|
|
171
|
+
</Link>
|
|
172
|
+
<Link href="/leads"
|
|
173
|
+
className="px-3 py-2 rounded-lg text-navbar-nav-foreground hover:bg-navbar-nav-hover transition-colors">
|
|
174
|
+
Leads
|
|
175
|
+
</Link>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</nav>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Card
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
<div className="bg-card border border-card-line rounded-xl overflow-hidden">
|
|
185
|
+
<div className="bg-card-header px-6 py-4 border-b border-card-divider">
|
|
186
|
+
<h3 className="text-foreground font-semibold">Title</h3>
|
|
187
|
+
</div>
|
|
188
|
+
<div className="px-6 py-4">
|
|
189
|
+
<p className="text-muted-foreground">Content</p>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="bg-card-footer px-6 py-3 border-t border-card-divider">
|
|
192
|
+
<button className="bg-primary text-primary-foreground px-4 py-2 rounded-lg hover:bg-primary-hover transition-colors">
|
|
193
|
+
Action
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Sidebar
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
<aside className="w-64 bg-sidebar border-r border-sidebar-line h-screen">
|
|
203
|
+
<nav className="p-4 space-y-1">
|
|
204
|
+
{navItems.map(item => (
|
|
205
|
+
<Link key={item.href} href={item.href}
|
|
206
|
+
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition-colors
|
|
207
|
+
${isActive(item.href)
|
|
208
|
+
? 'bg-sidebar-nav-active text-primary font-medium'
|
|
209
|
+
: 'text-sidebar-nav-foreground hover:bg-sidebar-nav-hover'}`}>
|
|
210
|
+
{item.icon}
|
|
211
|
+
{item.label}
|
|
212
|
+
</Link>
|
|
213
|
+
))}
|
|
214
|
+
</nav>
|
|
215
|
+
</aside>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Dropdown (with Preline JS)
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
<div className="hs-dropdown relative">
|
|
222
|
+
<button className="hs-dropdown-toggle px-4 py-2 bg-layer border border-layer-line rounded-lg text-layer-foreground hover:bg-layer-hover">
|
|
223
|
+
Options ▾
|
|
224
|
+
</button>
|
|
225
|
+
<div className="hs-dropdown-menu hidden bg-dropdown border border-dropdown-line rounded-lg shadow-lg mt-1 p-1 min-w-48">
|
|
226
|
+
<button className="w-full text-left px-3 py-2 rounded-md text-dropdown-item-foreground hover:bg-dropdown-item-hover">
|
|
227
|
+
Edit
|
|
228
|
+
</button>
|
|
229
|
+
<div className="border-t border-dropdown-divider my-1" />
|
|
230
|
+
<button className="w-full text-left px-3 py-2 rounded-md text-destructive hover:bg-dropdown-item-hover">
|
|
231
|
+
Delete
|
|
232
|
+
</button>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Theme Generator (CLI)
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
# Generate theme from config
|
|
241
|
+
npx preline-theme-generator /tmp/config.json ./resources/css/themes/brand.css
|
|
242
|
+
|
|
243
|
+
# Config format:
|
|
244
|
+
{
|
|
245
|
+
"name": "brand",
|
|
246
|
+
"hue": 250,
|
|
247
|
+
"style": "professional",
|
|
248
|
+
"useCustomDarkGray": true,
|
|
249
|
+
"tailwindGray": "neutral"
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Chart Tokens (Apexcharts)
|
|
254
|
+
|
|
255
|
+
```css
|
|
256
|
+
/* Charts require HEX for gradients — no oklch! */
|
|
257
|
+
--chart-colors-primary-hex: #2563eb;
|
|
258
|
+
--chart-colors-chart-1-hex: #8b5cf6;
|
|
259
|
+
--chart-colors-chart-2-hex: #06b6d4;
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## FORBIDDEN
|
|
263
|
+
|
|
264
|
+
| ❌ Don't | ✅ Do |
|
|
265
|
+
|---|---|
|
|
266
|
+
| Modify `theme.css` (base) | Create separate theme file |
|
|
267
|
+
| Put tokens inside `@theme` block | Tokens in selector blocks only |
|
|
268
|
+
| Use raw colors (`bg-blue-500`) | Use semantic tokens (`bg-primary`) |
|
|
269
|
+
| `oklch()` in chart `-hex` tokens | Use hex format for charts |
|
|
270
|
+
| Force HTML class changes | Theme activation via `data-theme` only |
|
|
271
|
+
| Invent token names | Follow Preline's naming system |
|
|
272
|
+
| `@apply` for component styles | React components with token classes |
|
|
273
|
+
| Skip `HSStaticMethods.autoInit()` | Always re-init after Inertia navigation |
|
|
@@ -1,94 +1,273 @@
|
|
|
1
|
-
#
|
|
1
|
+
# TailwindCSS 4 — CSS-First Design System
|
|
2
2
|
|
|
3
|
-
**ALWAYS invoke when writing Tailwind
|
|
3
|
+
**ALWAYS invoke when writing Tailwind classes, theming, or responsive layouts.**
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What Changed (v3 → v4)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
| v3 (Legacy) | v4 (Current) |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `tailwind.config.js` | CSS `@theme` directive |
|
|
10
|
+
| PostCSS plugin | Oxide engine (10x faster) |
|
|
11
|
+
| `@tailwind base/components/utilities` | `@import "tailwindcss"` |
|
|
12
|
+
| Colors in JS config | Colors as CSS variables |
|
|
13
|
+
| `@apply` everywhere | Components > `@apply` |
|
|
14
|
+
|
|
15
|
+
## Setup (v4)
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
/* resources/css/app.css (Laravel + Inertia) */
|
|
19
|
+
@import "tailwindcss";
|
|
20
|
+
|
|
21
|
+
@theme {
|
|
22
|
+
/* ─── Semantic Colors (OKLCH for perceptual uniformity) ─── */
|
|
23
|
+
--color-primary: oklch(0.55 0.15 250);
|
|
24
|
+
--color-primary-foreground: oklch(0.98 0 0);
|
|
25
|
+
--color-secondary: oklch(0.75 0.05 250);
|
|
26
|
+
--color-secondary-foreground: oklch(0.15 0 0);
|
|
27
|
+
--color-destructive: oklch(0.55 0.2 25);
|
|
28
|
+
--color-destructive-foreground: oklch(0.98 0 0);
|
|
29
|
+
|
|
30
|
+
--color-background: oklch(0.99 0 0);
|
|
31
|
+
--color-foreground: oklch(0.15 0 0);
|
|
32
|
+
--color-muted: oklch(0.95 0.01 250);
|
|
33
|
+
--color-muted-foreground: oklch(0.45 0.02 250);
|
|
34
|
+
--color-border: oklch(0.90 0.01 250);
|
|
35
|
+
--color-ring: oklch(0.55 0.15 250);
|
|
36
|
+
|
|
37
|
+
--color-surface: oklch(0.98 0 0);
|
|
38
|
+
--color-surface-raised: oklch(1 0 0);
|
|
39
|
+
|
|
40
|
+
/* ─── Typography ─── */
|
|
41
|
+
--font-sans: 'Inter', system-ui, sans-serif;
|
|
42
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
43
|
+
|
|
44
|
+
/* ─── Spacing ─── */
|
|
45
|
+
--spacing-page: 1rem;
|
|
46
|
+
|
|
47
|
+
/* ─── Border Radius ─── */
|
|
48
|
+
--radius-sm: 0.375rem;
|
|
49
|
+
--radius-md: 0.5rem;
|
|
50
|
+
--radius-lg: 0.75rem;
|
|
51
|
+
--radius-xl: 1rem;
|
|
52
|
+
|
|
53
|
+
/* ─── Shadows ─── */
|
|
54
|
+
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.05);
|
|
55
|
+
--shadow-md: 0 4px 6px oklch(0 0 0 / 0.07);
|
|
56
|
+
--shadow-lg: 0 10px 15px oklch(0 0 0 / 0.1);
|
|
57
|
+
|
|
58
|
+
/* ─── Animations ─── */
|
|
59
|
+
--animate-fade-in: fade-in 0.2s ease-out;
|
|
60
|
+
--animate-slide-up: slide-up 0.3s ease-out;
|
|
61
|
+
--animate-slide-down: slide-down 0.3s ease-out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ─── Dark Mode Override ─── */
|
|
65
|
+
@theme dark {
|
|
66
|
+
--color-background: oklch(0.13 0.01 250);
|
|
67
|
+
--color-foreground: oklch(0.95 0 0);
|
|
68
|
+
--color-surface: oklch(0.17 0.01 250);
|
|
69
|
+
--color-surface-raised: oklch(0.21 0.01 250);
|
|
70
|
+
--color-muted: oklch(0.22 0.02 250);
|
|
71
|
+
--color-muted-foreground: oklch(0.65 0.02 250);
|
|
72
|
+
--color-border: oklch(0.28 0.02 250);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ─── Keyframes ─── */
|
|
76
|
+
@keyframes fade-in {
|
|
77
|
+
from { opacity: 0; }
|
|
78
|
+
to { opacity: 1; }
|
|
79
|
+
}
|
|
80
|
+
@keyframes slide-up {
|
|
81
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
82
|
+
to { opacity: 1; transform: translateY(0); }
|
|
83
|
+
}
|
|
84
|
+
@keyframes slide-down {
|
|
85
|
+
from { opacity: 0; transform: translateY(-8px); }
|
|
86
|
+
to { opacity: 1; transform: translateY(0); }
|
|
87
|
+
}
|
|
9
88
|
```
|
|
10
89
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
{/* Mobile: full → Tablet: half → Desktop: third → Large: quarter */}
|
|
14
|
-
</div>
|
|
90
|
+
## Color Token Architecture
|
|
91
|
+
|
|
15
92
|
```
|
|
93
|
+
Layer 1 — Primitive: oklch(0.55 0.15 250) (raw values)
|
|
94
|
+
Layer 2 — Semantic: --color-primary (purpose-based)
|
|
95
|
+
Layer 3 — Component: className="bg-primary" (usage in JSX)
|
|
16
96
|
|
|
17
|
-
|
|
97
|
+
Usage in Tailwind classes:
|
|
98
|
+
bg-primary text-primary-foreground
|
|
99
|
+
bg-destructive text-destructive-foreground
|
|
100
|
+
bg-muted text-muted-foreground
|
|
101
|
+
bg-surface border-border
|
|
102
|
+
```
|
|
18
103
|
|
|
19
|
-
|
|
20
|
-
```tsx
|
|
21
|
-
// Center
|
|
22
|
-
<div className="flex items-center justify-center h-screen" />
|
|
104
|
+
## Mobile-First Responsive
|
|
23
105
|
|
|
24
|
-
|
|
25
|
-
|
|
106
|
+
```
|
|
107
|
+
Breakpoints (min-width):
|
|
108
|
+
(none) → 0px Mobile base
|
|
109
|
+
sm: → 640px Large phone
|
|
110
|
+
md: → 768px Tablet
|
|
111
|
+
lg: → 1024px Laptop
|
|
112
|
+
xl: → 1280px Desktop
|
|
113
|
+
2xl: → 1536px Large desktop
|
|
26
114
|
```
|
|
27
115
|
|
|
28
|
-
### Grid
|
|
29
116
|
```tsx
|
|
30
|
-
|
|
31
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
|
|
117
|
+
{/* Mobile: stack → Tablet: 2 cols → Desktop: 3 cols */}
|
|
118
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
32
119
|
|
|
33
|
-
|
|
34
|
-
<div className="
|
|
120
|
+
{/* Mobile: full width → Desktop: sidebar layout */}
|
|
121
|
+
<div className="flex flex-col lg:flex-row gap-6">
|
|
122
|
+
<aside className="w-full lg:w-64 shrink-0">Sidebar</aside>
|
|
123
|
+
<main className="flex-1">Content</main>
|
|
124
|
+
</div>
|
|
35
125
|
```
|
|
36
126
|
|
|
37
|
-
##
|
|
127
|
+
## Container Queries (v4 Native)
|
|
38
128
|
|
|
39
129
|
```tsx
|
|
40
|
-
|
|
41
|
-
|
|
130
|
+
{/* Parent defines container */}
|
|
131
|
+
<div className="@container">
|
|
132
|
+
{/* Children respond to PARENT width, not viewport */}
|
|
133
|
+
<div className="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3 gap-4">
|
|
134
|
+
<Card />
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Named containers for specificity */}
|
|
139
|
+
<div className="@container/card">
|
|
140
|
+
<h2 className="text-sm @md/card:text-lg">Title</h2>
|
|
42
141
|
</div>
|
|
43
142
|
```
|
|
44
143
|
|
|
144
|
+
**Rule:** Use container queries for **reusable components** (cards, widgets). Use viewport breakpoints for **page-level layouts**.
|
|
145
|
+
|
|
45
146
|
## Component Patterns
|
|
46
147
|
|
|
47
|
-
###
|
|
148
|
+
### Button
|
|
149
|
+
|
|
48
150
|
```tsx
|
|
49
|
-
|
|
151
|
+
const buttonVariants = {
|
|
152
|
+
base: "inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed",
|
|
153
|
+
variant: {
|
|
154
|
+
primary: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
155
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
156
|
+
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
157
|
+
outline: "border border-border bg-transparent hover:bg-muted",
|
|
158
|
+
ghost: "hover:bg-muted",
|
|
159
|
+
},
|
|
160
|
+
size: {
|
|
161
|
+
sm: "h-8 px-3 text-sm",
|
|
162
|
+
md: "h-10 px-4 text-sm",
|
|
163
|
+
lg: "h-12 px-6 text-base",
|
|
164
|
+
},
|
|
165
|
+
};
|
|
50
166
|
```
|
|
51
167
|
|
|
52
|
-
###
|
|
168
|
+
### Card
|
|
169
|
+
|
|
53
170
|
```tsx
|
|
54
|
-
<
|
|
171
|
+
<div className="bg-surface-raised rounded-lg border border-border shadow-sm p-6 hover:shadow-md transition-shadow">
|
|
172
|
+
<h3 className="text-lg font-semibold text-foreground">Title</h3>
|
|
173
|
+
<p className="mt-2 text-muted-foreground">Description</p>
|
|
174
|
+
</div>
|
|
55
175
|
```
|
|
56
176
|
|
|
57
177
|
### Input
|
|
178
|
+
|
|
58
179
|
```tsx
|
|
59
|
-
<input className="w-full px-3
|
|
180
|
+
<input className="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" />
|
|
60
181
|
```
|
|
61
182
|
|
|
62
|
-
|
|
183
|
+
### Table (responsive)
|
|
63
184
|
|
|
64
185
|
```tsx
|
|
65
|
-
|
|
66
|
-
<div className="
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
186
|
+
{/* Mobile: card layout → Desktop: table */}
|
|
187
|
+
<div className="hidden md:block">
|
|
188
|
+
<table className="w-full text-sm">
|
|
189
|
+
<thead className="border-b border-border">
|
|
190
|
+
<tr className="text-left text-muted-foreground">
|
|
191
|
+
<th className="p-3 font-medium">Name</th>
|
|
192
|
+
</tr>
|
|
193
|
+
</thead>
|
|
194
|
+
</table>
|
|
195
|
+
</div>
|
|
196
|
+
<div className="md:hidden space-y-3">
|
|
197
|
+
{/* Card layout for mobile */}
|
|
198
|
+
</div>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Dark Mode
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
{/* Automatic with semantic tokens */}
|
|
205
|
+
<div className="bg-background text-foreground">
|
|
206
|
+
<p className="text-muted-foreground">Always adapts</p>
|
|
207
|
+
<div className="border-border bg-surface">Card</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Toggle button */}
|
|
211
|
+
<button onClick={() => document.documentElement.classList.toggle('dark')}>
|
|
212
|
+
Toggle Theme
|
|
213
|
+
</button>
|
|
79
214
|
```
|
|
80
215
|
|
|
81
|
-
|
|
216
|
+
**Rule:** Use semantic tokens (`bg-background`, `text-foreground`) instead of explicit dark classes. The `@theme dark` block handles everything.
|
|
217
|
+
|
|
218
|
+
## Show/Hide
|
|
82
219
|
|
|
83
220
|
```tsx
|
|
84
221
|
<div className="hidden lg:block">Desktop only</div>
|
|
85
|
-
<div className="
|
|
222
|
+
<div className="lg:hidden">Mobile only</div>
|
|
86
223
|
<span className="sr-only">Screen reader only</span>
|
|
87
224
|
```
|
|
88
225
|
|
|
226
|
+
## Auto-fit Grid
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
{/* Cards that auto-wrap based on available space */}
|
|
230
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-4">
|
|
231
|
+
{items.map(item => <Card key={item.id} {...item} />)}
|
|
232
|
+
</div>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Animations
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
<div className="animate-fade-in">Fades in on mount</div>
|
|
239
|
+
<div className="animate-slide-up">Slides up on mount</div>
|
|
240
|
+
<Loader className="h-5 w-5 animate-spin" />
|
|
241
|
+
<div className="h-4 w-24 bg-muted animate-pulse rounded" /> {/* Skeleton */}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Spacing Consistency
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
Use the 4px scale consistently:
|
|
248
|
+
gap-1 = 4px p-1 = 4px
|
|
249
|
+
gap-2 = 8px p-2 = 8px
|
|
250
|
+
gap-3 = 12px p-3 = 12px
|
|
251
|
+
gap-4 = 16px p-4 = 16px
|
|
252
|
+
gap-6 = 24px p-6 = 24px
|
|
253
|
+
gap-8 = 32px p-8 = 32px
|
|
254
|
+
|
|
255
|
+
Page padding: px-4 md:px-6 lg:px-8
|
|
256
|
+
Section gaps: space-y-6 md:space-y-8
|
|
257
|
+
Card padding: p-4 md:p-6
|
|
258
|
+
```
|
|
259
|
+
|
|
89
260
|
## FORBIDDEN
|
|
90
261
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
262
|
+
| ❌ Don't | ✅ Do |
|
|
263
|
+
|---|---|
|
|
264
|
+
| `tailwind.config.js` in v4 | `@theme` in CSS |
|
|
265
|
+
| `@apply` for everything | React components |
|
|
266
|
+
| `!important` | Fix specificity |
|
|
267
|
+
| `style={{ color: 'red' }}` | `text-destructive` |
|
|
268
|
+
| Arbitrary values for tokens | Define in `@theme` |
|
|
269
|
+
| `bg-white dark:bg-gray-900` | `bg-background` (semantic) |
|
|
270
|
+
| `@tailwind base` | `@import "tailwindcss"` (v4) |
|
|
271
|
+
| Dynamic class strings | Static complete strings (purge-safe) |
|
|
272
|
+
| `text-[#FF5733]` inline | `--color-accent` in `@theme` |
|
|
273
|
+
| Inconsistent spacing | Follow 4px scale |
|