@tailwind-styled/compiler 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.
@@ -0,0 +1,498 @@
1
+ /**
2
+ * tailwind-styled-v4 — Style Bucket System
3
+ *
4
+ * Setiap CSS rule masuk ke "bucket" berdasarkan tipe property-nya.
5
+ * Bucket di-emit dalam urutan yang selalu sama → CSS order stabil
6
+ * meskipun rule di-generate dari banyak file secara incremental.
7
+ *
8
+ * Tanpa bucket system:
9
+ * .tw-color { color: blue } ← dari file A
10
+ * .tw-flex { display: flex } ← dari file B
11
+ * .tw-color2 { color: red } ← dari file C
12
+ * → urutan output bergantung urutan file di-process = TIDAK STABIL
13
+ *
14
+ * Dengan bucket system:
15
+ * /* reset *\/
16
+ * /* layout *\/ → display, position, flex, grid, overflow
17
+ * /* spacing *\/ → margin, padding, gap, inset
18
+ * /* sizing *\/ → width, height, max/min-width/height
19
+ * /* typography *\/ → font-size, font-weight, line-height, text-*
20
+ * /* visual *\/ → color, background, border, shadow, opacity
21
+ * /* interaction *\/ → cursor, pointer-events, user-select, transition
22
+ * /* responsive *\/ → @media queries (selalu di akhir)
23
+ * → SELALU urutan ini, terlepas dari urutan file
24
+ *
25
+ * Keuntungan utama:
26
+ * 1. CSS output deterministic antar build (reproducible builds)
27
+ * 2. Specificity conflict sangat kecil — base selalu lebih awal dari responsive
28
+ * 3. Debug lebih mudah — tahu section mana rule berada
29
+ *
30
+ * Integrasi:
31
+ * import { BucketEngine, bucketSort } from "./styleBucketSystem"
32
+ *
33
+ * const engine = new BucketEngine()
34
+ * engine.add(styleNode)
35
+ * const css = engine.emit()
36
+ */
37
+
38
+ import type { StyleNode } from "./incrementalEngine"
39
+
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+ // Bucket Types
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * 8 bucket utama + 1 bucket "unknown" untuk fallback.
46
+ * Urutan angka = urutan emit di CSS output.
47
+ */
48
+ export type StyleBucket =
49
+ | "reset" // 0 — *, box-sizing, root
50
+ | "layout" // 1 — display, position, flex, grid, overflow, z-index
51
+ | "spacing" // 2 — margin, padding, gap, inset, top/right/bottom/left
52
+ | "sizing" // 3 — width, height, max/min variants
53
+ | "typography" // 4 — font-size, font-weight, line-height, letter-spacing, text-align
54
+ | "visual" // 5 — color, background, border, border-radius, shadow, opacity, outline
55
+ | "interaction" // 6 — cursor, pointer-events, user-select, transition, transform, animation
56
+ | "responsive" // 7 — @media queries (semua)
57
+ | "unknown" // 8 — fallback untuk property yang tidak dikenal
58
+
59
+ const BUCKET_ORDER: StyleBucket[] = [
60
+ "reset",
61
+ "layout",
62
+ "spacing",
63
+ "sizing",
64
+ "typography",
65
+ "visual",
66
+ "interaction",
67
+ "responsive",
68
+ "unknown",
69
+ ]
70
+
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+ // Property → Bucket Mapping
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+
75
+ /** Map dari CSS property prefix/exact ke bucket */
76
+ const PROPERTY_BUCKET_MAP: Record<string, StyleBucket> = {
77
+ // Layout
78
+ display: "layout",
79
+ position: "layout",
80
+ flex: "layout",
81
+ "flex-direction": "layout",
82
+ "flex-wrap": "layout",
83
+ "flex-grow": "layout",
84
+ "flex-shrink": "layout",
85
+ "flex-basis": "layout",
86
+ grid: "layout",
87
+ "grid-template": "layout",
88
+ "grid-column": "layout",
89
+ "grid-row": "layout",
90
+ "align-items": "layout",
91
+ "align-self": "layout",
92
+ "align-content": "layout",
93
+ "justify-content": "layout",
94
+ "justify-items": "layout",
95
+ "justify-self": "layout",
96
+ "place-items": "layout",
97
+ "place-content": "layout",
98
+ overflow: "layout",
99
+ "overflow-x": "layout",
100
+ "overflow-y": "layout",
101
+ "z-index": "layout",
102
+ float: "layout",
103
+ clear: "layout",
104
+ visibility: "layout",
105
+
106
+ // Spacing
107
+ padding: "spacing",
108
+ "padding-top": "spacing",
109
+ "padding-bottom": "spacing",
110
+ "padding-left": "spacing",
111
+ "padding-right": "spacing",
112
+ "padding-inline": "spacing",
113
+ "padding-block": "spacing",
114
+ margin: "spacing",
115
+ "margin-top": "spacing",
116
+ "margin-bottom": "spacing",
117
+ "margin-left": "spacing",
118
+ "margin-right": "spacing",
119
+ "margin-inline": "spacing",
120
+ "margin-block": "spacing",
121
+ gap: "spacing",
122
+ "column-gap": "spacing",
123
+ "row-gap": "spacing",
124
+ inset: "spacing",
125
+ "inset-inline": "spacing",
126
+ "inset-block": "spacing",
127
+ top: "spacing",
128
+ bottom: "spacing",
129
+ left: "spacing",
130
+ right: "spacing",
131
+
132
+ // Sizing
133
+ width: "sizing",
134
+ height: "sizing",
135
+ "max-width": "sizing",
136
+ "min-width": "sizing",
137
+ "max-height": "sizing",
138
+ "min-height": "sizing",
139
+ "aspect-ratio": "sizing",
140
+
141
+ // Typography
142
+ "font-size": "typography",
143
+ "font-weight": "typography",
144
+ "font-family": "typography",
145
+ "font-style": "typography",
146
+ "line-height": "typography",
147
+ "letter-spacing": "typography",
148
+ "text-align": "typography",
149
+ "text-decoration": "typography",
150
+ "text-transform": "typography",
151
+ "text-overflow": "typography",
152
+ "white-space": "typography",
153
+ "word-break": "typography",
154
+ "word-wrap": "typography",
155
+ "vertical-align": "typography",
156
+
157
+ // Visual
158
+ color: "visual",
159
+ background: "visual",
160
+ "background-color": "visual",
161
+ "background-image": "visual",
162
+ "background-size": "visual",
163
+ "background-position": "visual",
164
+ "background-repeat": "visual",
165
+ border: "visual",
166
+ "border-top": "visual",
167
+ "border-bottom": "visual",
168
+ "border-left": "visual",
169
+ "border-right": "visual",
170
+ "border-inline": "visual",
171
+ "border-block": "visual",
172
+ "border-color": "visual",
173
+ "border-width": "visual",
174
+ "border-style": "visual",
175
+ "border-radius": "visual",
176
+ "box-shadow": "visual",
177
+ opacity: "visual",
178
+ outline: "visual",
179
+ "outline-color": "visual",
180
+ "outline-width": "visual",
181
+ fill: "visual",
182
+ stroke: "visual",
183
+ "text-shadow": "visual",
184
+ "mix-blend-mode": "visual",
185
+ "object-fit": "visual",
186
+ "object-position": "visual",
187
+
188
+ // Interaction
189
+ cursor: "interaction",
190
+ "pointer-events": "interaction",
191
+ "user-select": "interaction",
192
+ transition: "interaction",
193
+ "transition-property": "interaction",
194
+ "transition-duration": "interaction",
195
+ "transition-timing-function": "interaction",
196
+ "transition-delay": "interaction",
197
+ transform: "interaction",
198
+ translate: "interaction",
199
+ rotate: "interaction",
200
+ scale: "interaction",
201
+ animation: "interaction",
202
+ "will-change": "interaction",
203
+ "scroll-behavior": "interaction",
204
+ "scroll-snap-type": "interaction",
205
+
206
+ // Reset (jarang dipakai langsung tapi handle untuk completeness)
207
+ "box-sizing": "reset",
208
+ appearance: "reset",
209
+ all: "reset",
210
+ }
211
+
212
+ // ─────────────────────────────────────────────────────────────────────────────
213
+ // Bucket Classifier
214
+ // ─────────────────────────────────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Classify satu StyleNode ke bucket yang tepat.
218
+ *
219
+ * Priority:
220
+ * 1. Jika ada modifier @media → "responsive" (selalu paling akhir)
221
+ * 2. Cek declaration property → lookup PROPERTY_BUCKET_MAP
222
+ * 3. Fallback ke "unknown"
223
+ */
224
+ export function classifyNode(node: StyleNode): StyleBucket {
225
+ // Media queries selalu masuk responsive bucket
226
+ if (node.modifier?.startsWith("@")) return "responsive"
227
+
228
+ // Extract property dari declaration "property: value; ..." (support multi-prop)
229
+ const declarations = node.declaration
230
+ .split(";")
231
+ .map((d: string) => d.trim())
232
+ .filter(Boolean)
233
+ const firstProp = declarations[0]?.split(":")[0]?.trim()
234
+
235
+ if (!firstProp) return "unknown"
236
+
237
+ // Exact match
238
+ if (PROPERTY_BUCKET_MAP[firstProp]) return PROPERTY_BUCKET_MAP[firstProp]
239
+
240
+ // Prefix match — untuk shorthand variants
241
+ for (const [prefix, bucket] of Object.entries(PROPERTY_BUCKET_MAP)) {
242
+ if (firstProp.startsWith(prefix)) return bucket
243
+ }
244
+
245
+ return "unknown"
246
+ }
247
+
248
+ // ─────────────────────────────────────────────────────────────────────────────
249
+ // Bucket Engine
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+
252
+ export interface BucketStats {
253
+ totalNodes: number
254
+ perBucket: Record<StyleBucket, number>
255
+ }
256
+
257
+ /**
258
+ * BucketEngine — menyimpan dan emit CSS dalam urutan bucket yang stabil.
259
+ *
260
+ * @example
261
+ * const engine = new BucketEngine()
262
+ * for (const node of styleNodes) engine.add(node)
263
+ * const css = engine.emit()
264
+ */
265
+ export class BucketEngine {
266
+ private buckets: Map<StyleBucket, Map<string, StyleNode>>
267
+
268
+ constructor() {
269
+ this.buckets = new Map()
270
+ for (const b of BUCKET_ORDER) {
271
+ this.buckets.set(b, new Map())
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Tambah StyleNode ke bucket yang tepat.
277
+ * Idempotent — atomic class yang sama tidak akan duplikat.
278
+ */
279
+ add(node: StyleNode): void {
280
+ const bucket = classifyNode(node)
281
+ this.buckets.get(bucket)!.set(node.atomicClass, node)
282
+ }
283
+
284
+ /**
285
+ * Hapus node dari bucket (untuk incremental update).
286
+ */
287
+ remove(atomicClass: string): void {
288
+ for (const bucket of this.buckets.values()) {
289
+ if (bucket.delete(atomicClass)) break
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Apply CssDiff dari incremental engine.
295
+ */
296
+ applyDiff(diff: { added: StyleNode[]; removed: string[] }): void {
297
+ for (const node of diff.added) this.add(node)
298
+ for (const cls of diff.removed) this.remove(cls)
299
+ }
300
+
301
+ /**
302
+ * Emit seluruh CSS dalam urutan bucket yang deterministic.
303
+ *
304
+ * @param comments - Tambahkan komentar section per bucket. Default: true
305
+ * @returns CSS string yang siap di-write ke file
306
+ */
307
+ emit(comments = true): string {
308
+ const sections: string[] = []
309
+
310
+ for (const bucketName of BUCKET_ORDER) {
311
+ const nodes = this.buckets.get(bucketName)!
312
+ if (nodes.size === 0) continue
313
+
314
+ const rules: string[] = []
315
+
316
+ for (const node of nodes.values()) {
317
+ rules.push(nodeToCSS(node))
318
+ }
319
+
320
+ if (rules.length === 0) continue
321
+
322
+ if (comments) {
323
+ sections.push(`/* ── ${bucketName} ── */`)
324
+ }
325
+ sections.push(...rules)
326
+ }
327
+
328
+ return sections.join("\n")
329
+ }
330
+
331
+ /**
332
+ * Emit dengan @layer CSS untuk native browser layering.
333
+ * Lebih powerful — browser respects layer order untuk specificity.
334
+ *
335
+ * @example output:
336
+ * @layer tw-layout, tw-spacing, tw-visual, tw-responsive;
337
+ * @layer tw-layout { .tw-a1 { display: flex } }
338
+ */
339
+ emitLayered(): string {
340
+ // Layer declaration
341
+ const layerNames = BUCKET_ORDER.filter(
342
+ (b) => b !== "unknown" && this.buckets.get(b)!.size > 0
343
+ ).map((b) => `tw-${b}`)
344
+
345
+ if (layerNames.length === 0) return ""
346
+
347
+ const parts: string[] = [`@layer ${layerNames.join(", ")};`, ""]
348
+
349
+ for (const bucketName of BUCKET_ORDER) {
350
+ const nodes = this.buckets.get(bucketName)!
351
+ if (nodes.size === 0) continue
352
+
353
+ const rules = Array.from(nodes.values()).map(nodeToCSS).join("\n ")
354
+ parts.push(`@layer tw-${bucketName} {\n ${rules}\n}`)
355
+ }
356
+
357
+ return parts.join("\n")
358
+ }
359
+
360
+ /** Semua nodes dari semua bucket (untuk full registry access) */
361
+ allNodes(): StyleNode[] {
362
+ const all: StyleNode[] = []
363
+ for (const bucket of this.buckets.values()) {
364
+ for (const node of bucket.values()) {
365
+ all.push(node)
366
+ }
367
+ }
368
+ return all
369
+ }
370
+
371
+ /** Stats per bucket */
372
+ stats(): BucketStats {
373
+ const perBucket = {} as Record<StyleBucket, number>
374
+ let total = 0
375
+ for (const [name, nodes] of this.buckets) {
376
+ perBucket[name] = nodes.size
377
+ total += nodes.size
378
+ }
379
+ return { totalNodes: total, perBucket }
380
+ }
381
+
382
+ /** Clear semua bucket */
383
+ clear(): void {
384
+ for (const bucket of this.buckets.values()) {
385
+ bucket.clear()
386
+ }
387
+ }
388
+ }
389
+
390
+ // ─────────────────────────────────────────────────────────────────────────────
391
+ // bucketSort — utility function untuk sort array StyleNodes
392
+ // ─────────────────────────────────────────────────────────────────────────────
393
+
394
+ /**
395
+ * Sort array StyleNodes dalam urutan bucket.
396
+ * Berguna untuk one-off sorting tanpa perlu BucketEngine instance.
397
+ *
398
+ * @example
399
+ * const sorted = bucketSort(allNodes)
400
+ * const css = sorted.map(nodeToCSS).join("\n")
401
+ */
402
+ export function bucketSort(nodes: StyleNode[]): StyleNode[] {
403
+ const bucketIndex = Object.fromEntries(BUCKET_ORDER.map((b, i) => [b, i])) as Record<
404
+ StyleBucket,
405
+ number
406
+ >
407
+
408
+ return [...nodes].sort((a, b) => {
409
+ const ai = bucketIndex[classifyNode(a)]
410
+ const bi = bucketIndex[classifyNode(b)]
411
+ return ai - bi
412
+ })
413
+ }
414
+
415
+ // ─────────────────────────────────────────────────────────────────────────────
416
+ // CSS generation helpers
417
+ // ─────────────────────────────────────────────────────────────────────────────
418
+
419
+ function nodeToCSS(node: StyleNode): string {
420
+ const { atomicClass, declaration, modifier } = node
421
+
422
+ if (!modifier) {
423
+ return `.${atomicClass}{${declaration}}`
424
+ }
425
+
426
+ if (modifier.startsWith("@")) {
427
+ return `${modifier}{.${atomicClass}{${declaration}}}`
428
+ }
429
+
430
+ return `.${atomicClass}${modifier}{${declaration}}`
431
+ }
432
+
433
+ // ─────────────────────────────────────────────────────────────────────────────
434
+ // CSSConflictDetector — dev-mode helper
435
+ // ─────────────────────────────────────────────────────────────────────────────
436
+
437
+ export interface ConflictWarning {
438
+ property: string
439
+ classes: string[]
440
+ bucket: StyleBucket
441
+ message: string
442
+ }
443
+
444
+ /**
445
+ * Detect potential CSS conflicts dalam satu set StyleNodes.
446
+ * Hanya untuk dev mode — tidak perlu run di production.
447
+ *
448
+ * Conflict = dua node dengan property yang sama tapi value berbeda
449
+ * di bucket yang sama (bukan responsive override).
450
+ *
451
+ * @example
452
+ * const warnings = detectConflicts(nodes)
453
+ * if (warnings.length) console.warn(warnings)
454
+ */
455
+ export function detectConflicts(nodes: StyleNode[]): ConflictWarning[] {
456
+ // property+modifier → node
457
+ const seen = new Map<string, StyleNode>()
458
+ const warnings: ConflictWarning[] = []
459
+
460
+ for (const node of nodes) {
461
+ // Skip responsive — by design override base
462
+ if (node.modifier?.startsWith("@")) continue
463
+
464
+ const firstProp = node.declaration.split(":")[0]?.trim()
465
+ if (!firstProp) continue
466
+
467
+ const key = `${firstProp}::${node.modifier ?? ""}`
468
+ const prev = seen.get(key)
469
+
470
+ if (prev) {
471
+ warnings.push({
472
+ property: firstProp,
473
+ classes: [prev.twClass, node.twClass],
474
+ bucket: classifyNode(node),
475
+ message: `Possible conflict: "${prev.twClass}" and "${node.twClass}" both set "${firstProp}"`,
476
+ })
477
+ } else {
478
+ seen.set(key, node)
479
+ }
480
+ }
481
+
482
+ return warnings
483
+ }
484
+
485
+ // ─────────────────────────────────────────────────────────────────────────────
486
+ // Singleton BucketEngine
487
+ // ─────────────────────────────────────────────────────────────────────────────
488
+
489
+ let _bucketEngine: BucketEngine | null = null
490
+
491
+ export function getBucketEngine(): BucketEngine {
492
+ if (!_bucketEngine) _bucketEngine = new BucketEngine()
493
+ return _bucketEngine
494
+ }
495
+
496
+ export function resetBucketEngine(): void {
497
+ _bucketEngine = null
498
+ }