tailwind-styled-v4 1.0.0 โ 1.0.1
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 +467 -189
- package/dist/cli/init.js +208 -0
- package/dist/index.d.mts +63 -34
- package/dist/index.d.ts +63 -34
- package/dist/index.js +3 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +23 -77
package/README.md
CHANGED
|
@@ -1,50 +1,70 @@
|
|
|
1
|
-
# tailwind-styled-v4
|
|
1
|
+
# ๐จ tailwind-styled-v4
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **styled-components meets Tailwind CSS** โ dengan type safety penuh, zero DOM pollution, dan zero-runtime build output.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```tsx
|
|
6
|
+
const Button = tw.button({
|
|
7
|
+
base: "px-4 py-2 rounded-lg font-medium transition",
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
primary: "bg-blue-500 text-white hover:bg-blue-600",
|
|
11
|
+
ghost: "bg-transparent border hover:bg-zinc-800",
|
|
12
|
+
},
|
|
13
|
+
size: { sm: "h-8 text-sm", lg: "h-12 text-base" },
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: { variant: "primary", size: "sm" },
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
<Button variant="primary" size="lg" p={4} rounded="card" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## โจ Fitur Utama
|
|
24
|
+
|
|
25
|
+
| Fitur | Keterangan |
|
|
26
|
+
|-------|------------|
|
|
27
|
+
| ๐งฑ **styled-components API** | Familiar, chainable, SSR-safe |
|
|
28
|
+
| ๐ญ **Variant System** | `variants`, `compoundVariants`, `defaultVariants` |
|
|
29
|
+
| ๐ **Responsive Props** | `p={{ sm: 2, lg: 8 }}` โ responsive object syntax |
|
|
30
|
+
| ๐จ **Design Tokens** | `bg="primary"` โ `bg-blue-600` via theme store |
|
|
31
|
+
| โก **Zero-Runtime** | Build-time transform โ static `className` strings |
|
|
32
|
+
| ๐ **Plugin System** | Register custom prop resolvers |
|
|
33
|
+
| ๐ก๏ธ **Type Safe** | Full TypeScript generics & inference |
|
|
34
|
+
| ๐ซ **Zero DOM Pollution** | Props tidak bocor ke HTML attributes |
|
|
6
35
|
|
|
7
36
|
---
|
|
8
37
|
|
|
9
|
-
##
|
|
38
|
+
## ๐ฆ Instalasi
|
|
10
39
|
|
|
11
40
|
```bash
|
|
12
|
-
npm install tailwind-styled-v4
|
|
13
|
-
# peerDependencies
|
|
14
|
-
npm install styled-components tailwind-merge clsx
|
|
41
|
+
npm install tailwind-styled-v4 styled-components tailwind-merge
|
|
15
42
|
```
|
|
16
43
|
|
|
17
44
|
---
|
|
18
45
|
|
|
19
|
-
## Quick Start
|
|
46
|
+
## ๐ Quick Start
|
|
20
47
|
|
|
21
|
-
### Template Literal (
|
|
48
|
+
### 1๏ธโฃ Template Literal (paling simpel)
|
|
22
49
|
|
|
23
50
|
```tsx
|
|
24
51
|
import { tw } from "tailwind-styled-v4"
|
|
25
52
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
rounded-2xl
|
|
30
|
-
border border-zinc-800
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
// Responsive props inline
|
|
34
|
-
<Card p={{ base: 4, md: 6, lg: 8 }} />
|
|
35
|
-
// โ p-4 md:p-6 lg:p-8
|
|
53
|
+
const Box = tw.div`p-4 bg-zinc-900 rounded-xl`
|
|
54
|
+
const Title = tw.h1`text-2xl font-bold text-white`
|
|
55
|
+
const Badge = tw.span`px-2 py-1 rounded-full text-xs bg-blue-500 text-white`
|
|
36
56
|
```
|
|
37
57
|
|
|
38
|
-
### Object Config
|
|
58
|
+
### 2๏ธโฃ Object Config + Variants
|
|
39
59
|
|
|
40
60
|
```tsx
|
|
41
61
|
const Button = tw.button({
|
|
42
|
-
base: "inline-flex items-center
|
|
62
|
+
base: "inline-flex items-center font-medium transition rounded-lg",
|
|
43
63
|
variants: {
|
|
44
64
|
variant: {
|
|
45
|
-
primary: "bg-blue-
|
|
65
|
+
primary: "bg-blue-500 text-white hover:bg-blue-600",
|
|
46
66
|
ghost: "bg-transparent border border-zinc-700 hover:bg-zinc-800",
|
|
47
|
-
danger: "bg-red-
|
|
67
|
+
danger: "bg-red-500 text-white hover:bg-red-600",
|
|
48
68
|
},
|
|
49
69
|
size: {
|
|
50
70
|
sm: "h-8 px-3 text-sm",
|
|
@@ -53,278 +73,536 @@ const Button = tw.button({
|
|
|
53
73
|
},
|
|
54
74
|
},
|
|
55
75
|
compoundVariants: [
|
|
56
|
-
{ variant: "primary", size: "lg", class: "shadow-lg shadow-blue-
|
|
76
|
+
{ variant: "primary", size: "lg", class: "shadow-lg shadow-blue-500/30" },
|
|
57
77
|
],
|
|
58
78
|
defaultVariants: { variant: "primary", size: "md" },
|
|
59
79
|
})
|
|
60
80
|
|
|
61
81
|
// Usage
|
|
62
|
-
<Button variant="
|
|
63
|
-
<Button variant="
|
|
82
|
+
<Button variant="ghost" size="lg" />
|
|
83
|
+
<Button variant="danger" />
|
|
64
84
|
```
|
|
65
85
|
|
|
66
|
-
###
|
|
86
|
+
### 3๏ธโฃ Prop Shorthand (responsive-ready)
|
|
67
87
|
|
|
68
88
|
```tsx
|
|
69
|
-
|
|
89
|
+
// Scalar
|
|
90
|
+
<Box p={4} bg="zinc-900" rounded="xl" flex />
|
|
91
|
+
|
|
92
|
+
// Responsive object
|
|
93
|
+
<Box p={{ base: 2, md: 4, lg: 8 }} cols={{ sm: 1, md: 2, lg: 3 }} />
|
|
94
|
+
|
|
95
|
+
// Design token
|
|
96
|
+
<Box bg="primary" text="foreground" rounded="card" shadow="modal" />
|
|
70
97
|
```
|
|
71
98
|
|
|
72
|
-
###
|
|
99
|
+
### 4๏ธโฃ Extend Component
|
|
73
100
|
|
|
74
101
|
```tsx
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const NavLink = tw(Link)`text-zinc-400 hover:text-zinc-100 transition-colors`
|
|
102
|
+
const Card = tw.div`p-6 rounded-2xl bg-zinc-900`
|
|
103
|
+
const HeroCard = Card.extend`shadow-2xl ring-1 ring-zinc-700`
|
|
104
|
+
const DarkCard = Card.withVariants({
|
|
105
|
+
variants: { elevated: { true: "shadow-2xl", false: "" } }
|
|
106
|
+
})
|
|
81
107
|
```
|
|
82
108
|
|
|
83
|
-
###
|
|
109
|
+
### 5๏ธโฃ Wrap Komponen Apapun
|
|
84
110
|
|
|
85
111
|
```tsx
|
|
86
|
-
|
|
112
|
+
// shadcn/ui, Radix, komponen custom
|
|
113
|
+
const TwButton = withTw(Button, "px-4 py-2")
|
|
114
|
+
const TwDialog = tw(Dialog.Content)`p-6 rounded-2xl`
|
|
115
|
+
|
|
116
|
+
<TwButton p={4} rounded="xl" bg="zinc-900" />
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## ๐ญ Variant System
|
|
87
122
|
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
### Basic Variants
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
const Alert = tw.div({
|
|
127
|
+
base: "p-4 rounded-lg border",
|
|
90
128
|
variants: {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
129
|
+
status: {
|
|
130
|
+
info: "bg-sky-50 border-sky-200 text-sky-800",
|
|
131
|
+
success: "bg-emerald-50 border-emerald-200 text-emerald-800",
|
|
132
|
+
warning: "bg-amber-50 border-amber-200 text-amber-800",
|
|
133
|
+
danger: "bg-red-50 border-red-200 text-red-800",
|
|
134
|
+
},
|
|
96
135
|
},
|
|
97
|
-
defaultVariants: {
|
|
136
|
+
defaultVariants: { status: "info" },
|
|
98
137
|
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Compound Variants
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// Class tambahan ketika 2+ variant aktif bersamaan
|
|
144
|
+
compoundVariants: [
|
|
145
|
+
{ variant: "primary", size: "lg", class: "shadow-lg shadow-blue-500/30" },
|
|
146
|
+
{ variant: "ghost", size: "sm", class: "opacity-70" },
|
|
147
|
+
]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### withVariants โ tambah variant ke komponen existing
|
|
99
151
|
|
|
100
|
-
|
|
101
|
-
|
|
152
|
+
```tsx
|
|
153
|
+
const EnhancedButton = Button.withVariants({
|
|
154
|
+
variants: {
|
|
155
|
+
loading: { true: "opacity-50 cursor-not-allowed", false: "" }
|
|
156
|
+
}
|
|
157
|
+
})
|
|
102
158
|
```
|
|
103
159
|
|
|
104
160
|
---
|
|
105
161
|
|
|
106
|
-
##
|
|
162
|
+
## ๐ Prop Engine
|
|
163
|
+
|
|
164
|
+
Semua prop otomatis di-convert ke Tailwind class. Tidak perlu `className="..."`.
|
|
165
|
+
|
|
166
|
+
### Spacing
|
|
167
|
+
```tsx
|
|
168
|
+
<Box p={4} px={6} py={2} m="auto" mx={4} gap={3} />
|
|
169
|
+
// โ p-4 px-6 py-2 m-auto mx-4 gap-3
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Flexbox & Grid
|
|
173
|
+
```tsx
|
|
174
|
+
<Box flex items="center" justify="between" wrap />
|
|
175
|
+
<Box cols={3} rows={2} colSpan={2} gap={4} />
|
|
176
|
+
// โ flex items-center justify-between flex-wrap
|
|
177
|
+
// โ grid-cols-3 grid-rows-2 col-span-2 gap-4
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Colors & Typography
|
|
181
|
+
```tsx
|
|
182
|
+
<Box bg="zinc-900" text="white" border ring="blue-500" />
|
|
183
|
+
<Box fontSize="heading" font="sans" leading="relaxed" tracking="tight" />
|
|
184
|
+
```
|
|
107
185
|
|
|
108
|
-
|
|
186
|
+
### Layout & Position
|
|
187
|
+
```tsx
|
|
188
|
+
<Box pos="absolute" top={0} right={0} z={50} inset="x-4" />
|
|
189
|
+
<Box overflow="hidden" rounded="xl" shadow="card" opacity={90} />
|
|
190
|
+
```
|
|
109
191
|
|
|
192
|
+
### Responsive Object
|
|
110
193
|
```tsx
|
|
194
|
+
// Semua prop mendukung responsive object
|
|
111
195
|
<Box
|
|
112
196
|
p={{ base: 2, sm: 4, md: 6, lg: 8 }}
|
|
113
|
-
cols={{ base: 1,
|
|
114
|
-
|
|
115
|
-
display="grid"
|
|
197
|
+
cols={{ base: 1, md: 2, lg: 3 }}
|
|
198
|
+
hidden={{ base: true, md: false }}
|
|
116
199
|
/>
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Available Props
|
|
121
|
-
|
|
122
|
-
| Category | Props |
|
|
123
|
-
|----------|-------|
|
|
124
|
-
| **Padding** | `p` `px` `py` `pt` `pb` `pl` `pr` |
|
|
125
|
-
| **Margin** | `m` `mx` `my` `mt` `mb` `ml` `mr` |
|
|
126
|
-
| **Gap** | `gap` `gapX` `gapY` `spaceX` `spaceY` |
|
|
127
|
-
| **Sizing** | `w` `h` `size` `minW` `maxW` `minH` `maxH` |
|
|
128
|
-
| **Colors** | `bg` `text` `border` `ring` `fill` `stroke` |
|
|
129
|
-
| **Typography** | `font` `fontSize` `leading` `tracking` `align` `truncate` `lineClamp` |
|
|
130
|
-
| **Flexbox** | `flex` `flexDir` `items` `justify` `wrap` `grow` `shrink` `basis` `self` |
|
|
131
|
-
| **Grid** | `cols` `rows` `colSpan` `rowSpan` `colStart` `colEnd` |
|
|
132
|
-
| **Layout** | `display` `overflow` `pos` `z` `top` `right` `bottom` `left` `inset` |
|
|
133
|
-
| **Border** | `rounded` `roundedT` `roundedB` `borderStyle` `outline` |
|
|
134
|
-
| **Effects** | `opacity` `shadow` `blur` `brightness` `contrast` `grayscale` `backdrop` |
|
|
135
|
-
| **Motion** | `transition` `duration` `ease` `delay` `animate` |
|
|
136
|
-
| **Interaction** | `cursor` `select` `pointer` `touch` |
|
|
200
|
+
```
|
|
137
201
|
|
|
138
202
|
---
|
|
139
203
|
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
Di production build, semua `tw.tag\`...\`` dikompilasi menjadi static string โ zero JS runtime overhead.
|
|
204
|
+
## ๐จ Theme & Design Tokens
|
|
143
205
|
|
|
144
|
-
|
|
145
|
-
// next.config.ts
|
|
146
|
-
import { withTailwindStyled } from "tailwind-styled-v4/compiler"
|
|
206
|
+
### Setup
|
|
147
207
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
})(/** nextConfig */ {})
|
|
208
|
+
```tsx
|
|
209
|
+
// app/layout.tsx
|
|
210
|
+
import { loadDefaultTheme } from "tailwind-styled-v4"
|
|
211
|
+
loadDefaultTheme()
|
|
153
212
|
```
|
|
154
213
|
|
|
155
|
-
|
|
214
|
+
### Custom Theme
|
|
215
|
+
|
|
156
216
|
```tsx
|
|
157
|
-
|
|
217
|
+
import { setTheme } from "tailwind-styled-v4"
|
|
218
|
+
|
|
219
|
+
setTheme({
|
|
220
|
+
colors: {
|
|
221
|
+
primary: "blue-600",
|
|
222
|
+
secondary: "violet-600",
|
|
223
|
+
brand: "cyan-400",
|
|
224
|
+
background: "zinc-950",
|
|
225
|
+
surface: "zinc-900",
|
|
226
|
+
muted: "zinc-500",
|
|
227
|
+
},
|
|
228
|
+
radius: {
|
|
229
|
+
card: "2xl",
|
|
230
|
+
button: "lg",
|
|
231
|
+
badge: "full",
|
|
232
|
+
input: "md",
|
|
233
|
+
},
|
|
234
|
+
spacing: {
|
|
235
|
+
section: "16",
|
|
236
|
+
page: "24",
|
|
237
|
+
},
|
|
238
|
+
duration: {
|
|
239
|
+
fast: "150",
|
|
240
|
+
normal: "200",
|
|
241
|
+
slow: "300",
|
|
242
|
+
},
|
|
243
|
+
})
|
|
158
244
|
```
|
|
159
245
|
|
|
160
|
-
|
|
246
|
+
### Penggunaan Token
|
|
247
|
+
|
|
161
248
|
```tsx
|
|
162
|
-
|
|
249
|
+
// Token otomatis di-resolve
|
|
250
|
+
<Box bg="primary" /> // โ bg-blue-600
|
|
251
|
+
<Box rounded="card" /> // โ rounded-2xl
|
|
252
|
+
<Box p="section" /> // โ p-16
|
|
253
|
+
<Box duration="fast" /> // โ duration-150
|
|
163
254
|
```
|
|
164
255
|
|
|
165
|
-
Runtime cost: **0 bytes** untuk class resolution.
|
|
166
|
-
|
|
167
256
|
---
|
|
168
257
|
|
|
169
|
-
##
|
|
258
|
+
## ๐ฏ cv() โ Standalone Class Variants
|
|
170
259
|
|
|
171
|
-
|
|
172
|
-
// vite.config.ts
|
|
173
|
-
import { tailwindStyledPlugin } from "tailwind-styled-v4/compiler"
|
|
260
|
+
Cocok untuk **shadcn/ui**, **Radix UI**, atau komponen headless apapun tanpa styled-components.
|
|
174
261
|
|
|
175
|
-
|
|
176
|
-
|
|
262
|
+
```tsx
|
|
263
|
+
import { cv } from "tailwind-styled-v4"
|
|
264
|
+
|
|
265
|
+
const buttonVariants = cv({
|
|
266
|
+
base: "inline-flex items-center rounded-lg font-medium transition",
|
|
267
|
+
variants: {
|
|
268
|
+
variant: {
|
|
269
|
+
primary: "bg-blue-500 text-white hover:bg-blue-600",
|
|
270
|
+
ghost: "bg-transparent border hover:bg-zinc-800",
|
|
271
|
+
},
|
|
272
|
+
size: {
|
|
273
|
+
sm: "h-8 px-3 text-sm",
|
|
274
|
+
md: "h-10 px-4 text-sm",
|
|
275
|
+
lg: "h-12 px-6 text-base",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
compoundVariants: [
|
|
279
|
+
{ variant: "primary", size: "lg", class: "shadow-lg" }
|
|
280
|
+
],
|
|
281
|
+
defaultVariants: { variant: "primary", size: "md" },
|
|
177
282
|
})
|
|
283
|
+
|
|
284
|
+
// Pakai di komponen apapun
|
|
285
|
+
function Button({ variant, size, className, ...props }) {
|
|
286
|
+
return <button className={buttonVariants({ variant, size, className })} {...props} />
|
|
287
|
+
}
|
|
178
288
|
```
|
|
179
289
|
|
|
180
290
|
---
|
|
181
291
|
|
|
182
|
-
## Plugin System
|
|
292
|
+
## ๐ Plugin System
|
|
293
|
+
|
|
294
|
+
### Built-in Plugins
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import { loadBuiltinPlugins } from "tailwind-styled-v4"
|
|
298
|
+
loadBuiltinPlugins()
|
|
299
|
+
|
|
300
|
+
// Setelah load, props berikut tersedia:
|
|
301
|
+
<Box motionSafe backdropBlur="md" gradientFrom="blue-500" gradientTo="purple-500" />
|
|
302
|
+
<Box scrollSmooth printHidden autoRows="fr" />
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Tailwind v4 Plugins
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import { loadAllV4Plugins } from "tailwind-styled-v4"
|
|
309
|
+
loadAllV4Plugins()
|
|
310
|
+
|
|
311
|
+
// Typography
|
|
312
|
+
<article prose="lg" proseDark proseMaxW={false} />
|
|
313
|
+
// โ prose prose-lg dark:prose-invert max-w-none
|
|
314
|
+
|
|
315
|
+
// Forms
|
|
316
|
+
<input formInput />
|
|
317
|
+
<select formSelect />
|
|
318
|
+
|
|
319
|
+
// Aspect Ratio
|
|
320
|
+
<div aspectRatio="video" /> // โ aspect-video
|
|
321
|
+
<div aspectRatio="square" /> // โ aspect-square
|
|
322
|
+
|
|
323
|
+
// Animations
|
|
324
|
+
<div enterAnim="fade-up" /> // โ animate-in fade-in slide-in-from-bottom-4
|
|
325
|
+
<div exitAnim="fade" /> // โ animate-out fade-out
|
|
326
|
+
|
|
327
|
+
// Interactivity
|
|
328
|
+
<button hoverBg="zinc-800" hoverScale focusRing="blue-500" activeScale />
|
|
329
|
+
|
|
330
|
+
// Container Queries
|
|
331
|
+
<div cqContainer /> // โ @container
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Custom Plugin
|
|
183
335
|
|
|
184
336
|
```tsx
|
|
185
337
|
import { registerPlugin } from "tailwind-styled-v4"
|
|
186
338
|
|
|
187
|
-
// Custom plugin
|
|
188
339
|
registerPlugin({
|
|
189
340
|
name: "glass",
|
|
190
341
|
props: {
|
|
191
|
-
glass:
|
|
192
|
-
|
|
193
|
-
|
|
342
|
+
glass: (v) => v === true
|
|
343
|
+
? "backdrop-blur-md bg-white/10 border border-white/20"
|
|
344
|
+
: `backdrop-blur-${v} bg-white/10`,
|
|
345
|
+
},
|
|
194
346
|
})
|
|
195
347
|
|
|
196
|
-
//
|
|
197
|
-
<Box glass />
|
|
198
|
-
<Box
|
|
348
|
+
// Langsung bisa dipakai
|
|
349
|
+
<Box glass />
|
|
350
|
+
<Box glass="xl" />
|
|
199
351
|
```
|
|
200
352
|
|
|
201
|
-
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## โก Zero-Runtime Mode
|
|
356
|
+
|
|
357
|
+
### Next.js
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
// next.config.ts
|
|
361
|
+
import { withTailwindStyled } from "tailwind-styled-v4/compiler"
|
|
362
|
+
|
|
363
|
+
export default withTailwindStyled({
|
|
364
|
+
mode: "zero-runtime", // transform tw.div`...` โ static className
|
|
365
|
+
addDataAttr: true, // tambah data-tw="hash" untuk DevTools
|
|
366
|
+
})(nextConfig)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Vite
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
// vite.config.ts
|
|
373
|
+
import { tailwindStyledPlugin } from "tailwind-styled-v4/compiler"
|
|
374
|
+
|
|
375
|
+
export default defineConfig({
|
|
376
|
+
plugins: [
|
|
377
|
+
react(),
|
|
378
|
+
tailwindStyledPlugin({ mode: "zero-runtime" }),
|
|
379
|
+
],
|
|
380
|
+
})
|
|
381
|
+
```
|
|
202
382
|
|
|
383
|
+
**Before:**
|
|
203
384
|
```tsx
|
|
204
|
-
|
|
205
|
-
|
|
385
|
+
const Box = tw.div`p-4 bg-zinc-900 rounded-xl`
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**After (build output):**
|
|
389
|
+
```tsx
|
|
390
|
+
const Box = styled.div.attrs(() => ({ className: "p-4 bg-zinc-900 rounded-xl" }))``
|
|
206
391
|
```
|
|
207
392
|
|
|
393
|
+
> ๐ Nol JS runtime cost โ `className` jadi static string, bukan computed di setiap render.
|
|
394
|
+
|
|
208
395
|
---
|
|
209
396
|
|
|
210
|
-
##
|
|
397
|
+
## ๐ก๏ธ shouldForwardProp
|
|
398
|
+
|
|
399
|
+
Semua tw props **otomatis diblokir** dari forwarding ke DOM. Tidak ada warning `Unknown prop` di console.
|
|
211
400
|
|
|
212
401
|
```tsx
|
|
213
|
-
|
|
402
|
+
// โ
Tidak ada warning di DOM
|
|
403
|
+
<Box p={4} flex cols={3} bg="zinc-900" />
|
|
214
404
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
loadDarkTheme() // fintech/crypto/Web3 dark preset
|
|
405
|
+
// Transient props ($) selalu di-forward
|
|
406
|
+
<Box $isActive={true} />
|
|
218
407
|
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
408
|
+
// Manual block/allow
|
|
409
|
+
import { blockProp, allowProp } from "tailwind-styled-v4"
|
|
410
|
+
blockProp("customProp")
|
|
411
|
+
allowProp("size") // kalau mau size sampai ke DOM
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## ๐งช Validation (Dev Only)
|
|
417
|
+
|
|
418
|
+
```tsx
|
|
419
|
+
import { validateVariantProps, buildSchema } from "tailwind-styled-v4"
|
|
420
|
+
|
|
421
|
+
const schema = buildSchema("button", {
|
|
422
|
+
variant: { primary: "...", ghost: "..." },
|
|
423
|
+
size: { sm: "...", lg: "..." },
|
|
230
424
|
})
|
|
231
425
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
//
|
|
426
|
+
validateVariantProps({ variant: "invalid" }, schema)
|
|
427
|
+
// [tw-error] Invalid prop "variant"="invalid" on <button>
|
|
428
|
+
// Expected one of: "primary" | "ghost"
|
|
429
|
+
// Got: "invalid"
|
|
430
|
+
// Hint: Add "invalid" to the variants config, or fix the typo.
|
|
235
431
|
```
|
|
236
432
|
|
|
433
|
+
> โ ๏ธ Validation hanya aktif di `NODE_ENV !== "production"` โ **zero overhead di production**.
|
|
434
|
+
|
|
237
435
|
---
|
|
238
436
|
|
|
239
|
-
##
|
|
437
|
+
## ๐ง Utils
|
|
240
438
|
|
|
241
|
-
|
|
242
|
-
npm run build:safelist
|
|
243
|
-
```
|
|
439
|
+
### cx / cxm
|
|
244
440
|
|
|
245
|
-
|
|
441
|
+
```tsx
|
|
442
|
+
import { cx, cxm } from "tailwind-styled-v4"
|
|
246
443
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
444
|
+
// cx โ lightweight class joiner
|
|
445
|
+
cx("p-4", isActive && "bg-blue-500", ["rounded", "shadow"])
|
|
446
|
+
cx({ "p-4": true, "hidden": false })
|
|
250
447
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// ...
|
|
254
|
-
}
|
|
448
|
+
// cxm โ cx + tailwind-merge (conflict resolution)
|
|
449
|
+
cxm("p-2", "p-4") // โ "p-4" (p-2 dihapus)
|
|
255
450
|
```
|
|
256
451
|
|
|
257
|
-
|
|
452
|
+
### Class Parser
|
|
258
453
|
|
|
259
|
-
|
|
454
|
+
```tsx
|
|
455
|
+
import { parseClassString, splitModifiers, groupByModifier } from "tailwind-styled-v4"
|
|
260
456
|
|
|
261
|
-
|
|
457
|
+
splitModifiers("p-4 hover:bg-blue-500 md:p-8")
|
|
458
|
+
// โ { base: ["p-4"], modifiers: ["hover:bg-blue-500", "md:p-8"] }
|
|
262
459
|
|
|
263
|
-
|
|
264
|
-
bg-blue-500
|
|
265
|
-
hover:bg-blue-500 โ hover state
|
|
266
|
-
md:bg-blue-500 โ responsive
|
|
267
|
-
dark:bg-blue-500 โ dark mode
|
|
268
|
-
dark:hover:bg-blue-500 โ combined
|
|
460
|
+
groupByModifier("p-4 hover:bg-blue-500 hover:text-white")
|
|
461
|
+
// โ { "": ["p-4"], hover: ["bg-blue-500", "text-white"] }
|
|
269
462
|
```
|
|
270
463
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
]
|
|
279
|
-
|
|
464
|
+
### Safelist Generator
|
|
465
|
+
|
|
466
|
+
```tsx
|
|
467
|
+
import { generateSafelist } from "tailwind-styled-v4/compiler"
|
|
468
|
+
|
|
469
|
+
generateSafelist({
|
|
470
|
+
cwd: process.cwd(),
|
|
471
|
+
scanDirs: ["src", "app", "components"],
|
|
472
|
+
outputPath: "tailwind.safelist.json",
|
|
473
|
+
})
|
|
474
|
+
// โ Safelist: 247 classes โ /project/tailwind.safelist.json
|
|
280
475
|
```
|
|
281
476
|
|
|
282
477
|
---
|
|
283
478
|
|
|
284
|
-
##
|
|
479
|
+
## ๐ Struktur Engine
|
|
285
480
|
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
481
|
+
```
|
|
482
|
+
src/
|
|
483
|
+
โโโ core/
|
|
484
|
+
โ โโโ tw.ts ๐ฏ Proxy-free tw object (v2 fix)
|
|
485
|
+
โ โโโ createComponent.ts ๐๏ธ Single styled-chain factory
|
|
486
|
+
โ โโโ templateParser.ts ๐ Tagged template literal parser
|
|
487
|
+
โ โโโ cv.ts ๐ญ Standalone class variants
|
|
488
|
+
โ โโโ withTw.ts ๐ HOC wrapper untuk komponen apapun
|
|
489
|
+
โ โโโ styledFactory.ts ๐ญ Thin styled-components wrapper
|
|
490
|
+
โ
|
|
491
|
+
โโโ runtime/
|
|
492
|
+
โ โโโ propEngine.ts โ๏ธ 80+ prop โ class resolvers
|
|
493
|
+
โ โโโ variantEngine.ts ๐ญ Variant resolution
|
|
494
|
+
โ โโโ compoundVariant.ts ๐ Compound variant resolution
|
|
495
|
+
โ โโโ responsiveEngine.ts๐ Responsive object handler
|
|
496
|
+
โ โโโ classResolver.ts ๐ Full pipeline orchestrator
|
|
497
|
+
โ
|
|
498
|
+
โโโ theme/
|
|
499
|
+
โ โโโ themeStore.ts ๐จ Global token store
|
|
500
|
+
โ โโโ tokenResolver.ts ๐ Token โ class segment resolver
|
|
501
|
+
โ โโโ defaultTheme.ts ๐ฆ Ready-to-use semantic tokens
|
|
502
|
+
โ
|
|
503
|
+
โโโ plugins/
|
|
504
|
+
โ โโโ pluginEngine.ts ๐ Plugin registry & resolver
|
|
505
|
+
โ โโโ registerPlugin.ts ๐ Public plugin registration API
|
|
506
|
+
โ โโโ builtinPlugins.ts ๐งฐ Animation, grid, backdrop, gradient
|
|
507
|
+
โ โโโ tailwindV4Plugins.ts ๐ Typography, forms, aspect, animations
|
|
508
|
+
โ
|
|
509
|
+
โโโ compiler/
|
|
510
|
+
โ โโโ astTransform.ts ๐ Source code transformer (zero-runtime)
|
|
511
|
+
โ โโโ transformTw.ts ๐ ๏ธ Transform orchestrator
|
|
512
|
+
โ โโโ vitePlugin.ts โก Vite/Rollup plugin
|
|
513
|
+
โ โโโ swcPlugin.ts ๐ง Next.js SWC transform
|
|
514
|
+
โ โโโ webpackLoader.ts ๐ฆ Webpack loader
|
|
515
|
+
โ โโโ classExtractor.ts ๐ Class extraction for safelist
|
|
516
|
+
โ
|
|
517
|
+
โโโ validation/
|
|
518
|
+
โ โโโ propValidator.ts ๐ก๏ธ Dev-only prop validation
|
|
519
|
+
โ
|
|
520
|
+
โโโ utils/
|
|
521
|
+
โโโ cx.ts ๐ Lightweight class joiner
|
|
522
|
+
โโโ classParser.ts ๐ค Class string utilities
|
|
523
|
+
โโโ hash.ts #๏ธโฃ Stable hash generator
|
|
524
|
+
โโโ merge.ts ๐ twMerge wrapper
|
|
525
|
+
โโโ isObject.ts ๐ Type guard utility
|
|
292
526
|
```
|
|
293
527
|
|
|
294
528
|
---
|
|
295
529
|
|
|
296
|
-
##
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
530
|
+
## ๐งช Test Coverage
|
|
531
|
+
|
|
532
|
+
```
|
|
533
|
+
228 tests โ 228 pass โ
|
|
534
|
+
|
|
535
|
+
๐ต templateParser 9/9
|
|
536
|
+
๐ต themeStore 7/7
|
|
537
|
+
๐ต tokenResolver 7/7
|
|
538
|
+
๐ต defaultTheme 2/2
|
|
539
|
+
๐ต responsiveEngine 7/7
|
|
540
|
+
๐ต variantEngine 5/5
|
|
541
|
+
๐ต compoundVariant 5/5
|
|
542
|
+
๐ต propEngine 15/15
|
|
543
|
+
๐ต classResolver 2/2
|
|
544
|
+
๐ต pluginEngine 4/4
|
|
545
|
+
๐ต registerPlugin 3/3
|
|
546
|
+
๐ต builtinPlugins 7/7
|
|
547
|
+
๐ต tailwindV4Plugins 14/14
|
|
548
|
+
๐ต cv 4/4
|
|
549
|
+
๐ต propValidator 6/6
|
|
550
|
+
๐ต astTransform 10/10
|
|
551
|
+
๐ต utils 9/9
|
|
552
|
+
๐ต withTw 5/5
|
|
553
|
+
๐ต styledFactory 6/6
|
|
554
|
+
๐ต classParser 10/10
|
|
555
|
+
๐ต cx / cxm 8/8
|
|
556
|
+
๐ต classExtractor 4/4
|
|
557
|
+
๐ต generateSafelist 4/4
|
|
558
|
+
๐ต barrel exports 67/67
|
|
559
|
+
๐ต end-to-end 5/5
|
|
560
|
+
```
|
|
311
561
|
|
|
312
562
|
---
|
|
313
563
|
|
|
314
|
-
##
|
|
564
|
+
## ๐ Changelog v2
|
|
565
|
+
|
|
566
|
+
### ๐ด Bug Fixes
|
|
567
|
+
|
|
568
|
+
**`tw` undefined setelah bundle split (Vite/tsup)**
|
|
569
|
+
```
|
|
570
|
+
BEFORE: export const tw = new Proxy(fn, { get })
|
|
571
|
+
โ Proxy + tsup splitting = tw undefined di Next.js/Vite
|
|
572
|
+
|
|
573
|
+
AFTER: Object.assign(twCallable, { div, button, ... })
|
|
574
|
+
โ Plain object, transparan ke semua bundler
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Double wrap SSR hydration mismatch**
|
|
578
|
+
```
|
|
579
|
+
BEFORE: styled(tag)`` โ styled(BaseStyled).attrs()``
|
|
580
|
+
โ Double wrap = SSR hydration mismatch + SC v6 error
|
|
581
|
+
|
|
582
|
+
AFTER: styled(tag).withConfig().attrs()``
|
|
583
|
+
โ Single chain, satu SC instance per component
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
**shouldForwardProp hardcoded**
|
|
587
|
+
```
|
|
588
|
+
BEFORE: Hanya blokir variant names yang hardcoded
|
|
589
|
+
โ User variant keys bocor ke DOM
|
|
590
|
+
|
|
591
|
+
AFTER: Built dynamically dari variant keys aktual
|
|
592
|
+
โ Zero DOM pollution apapun nama variant-nya
|
|
593
|
+
```
|
|
315
594
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
- **Webpack** 4 / 5
|
|
595
|
+
**TypeScript generic inference**
|
|
596
|
+
```
|
|
597
|
+
BEFORE: TwTagFactory tidak generic
|
|
598
|
+
โ "Binding element '$isHover' implicitly has 'any' type"
|
|
599
|
+
|
|
600
|
+
AFTER: TwTagFactory<P> generic function
|
|
601
|
+
โ Full type inference di template interpolations
|
|
602
|
+
```
|
|
325
603
|
|
|
326
604
|
---
|
|
327
605
|
|
|
328
|
-
## License
|
|
606
|
+
## ๐ License
|
|
329
607
|
|
|
330
|
-
MIT
|
|
608
|
+
MIT ยฉ tailwind-styled-v4
|