tailwind-styled-v4 4.0.0 → 5.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.
Files changed (194) hide show
  1. package/CHANGELOG.md +398 -0
  2. package/LICENSE +21 -0
  3. package/README.md +532 -0
  4. package/dist/analyzer.d.mts +114 -0
  5. package/dist/analyzer.d.ts +114 -0
  6. package/dist/analyzer.js +1555 -0
  7. package/dist/analyzer.js.map +1 -0
  8. package/dist/analyzer.mjs +1544 -0
  9. package/dist/analyzer.mjs.map +1 -0
  10. package/dist/animate.d.mts +46 -0
  11. package/dist/animate.d.ts +41 -112
  12. package/dist/animate.js +792 -235
  13. package/dist/animate.js.map +1 -1
  14. package/dist/animate.mjs +782 -0
  15. package/dist/animate.mjs.map +1 -0
  16. package/dist/atomic.d.mts +18 -0
  17. package/dist/atomic.d.ts +18 -0
  18. package/dist/atomic.js +191 -0
  19. package/dist/atomic.js.map +1 -0
  20. package/dist/atomic.mjs +185 -0
  21. package/dist/atomic.mjs.map +1 -0
  22. package/dist/cli.d.mts +1 -0
  23. package/dist/cli.d.ts +1 -0
  24. package/dist/cli.js +6063 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/cli.mjs +6053 -0
  27. package/dist/cli.mjs.map +1 -0
  28. package/dist/{compiler.d.cts → compiler.d.mts} +503 -210
  29. package/dist/compiler.d.ts +503 -210
  30. package/dist/compiler.js +1549 -566
  31. package/dist/compiler.js.map +1 -1
  32. package/dist/{compiler.cjs → compiler.mjs} +1476 -627
  33. package/dist/compiler.mjs.map +1 -0
  34. package/dist/dashboard.d.mts +272 -0
  35. package/dist/dashboard.d.ts +272 -0
  36. package/dist/dashboard.js +249 -0
  37. package/dist/dashboard.js.map +1 -0
  38. package/dist/dashboard.mjs +239 -0
  39. package/dist/dashboard.mjs.map +1 -0
  40. package/dist/devtools.js +336 -211
  41. package/dist/devtools.js.map +1 -1
  42. package/dist/{devtools.cjs → devtools.mjs} +331 -220
  43. package/dist/devtools.mjs.map +1 -0
  44. package/dist/engine.d.mts +84 -0
  45. package/dist/engine.d.ts +84 -0
  46. package/dist/engine.js +3014 -0
  47. package/dist/engine.js.map +1 -0
  48. package/dist/engine.mjs +3005 -0
  49. package/dist/engine.mjs.map +1 -0
  50. package/dist/{index.d.cts → index.d.mts} +75 -4
  51. package/dist/index.d.ts +75 -4
  52. package/dist/index.js +1341 -149
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.mjs +2162 -0
  55. package/dist/index.mjs.map +1 -0
  56. package/dist/liveTokenEngine-DYN3Zale.d.mts +34 -0
  57. package/dist/liveTokenEngine-DYN3Zale.d.ts +34 -0
  58. package/dist/next.d.mts +55 -0
  59. package/dist/next.d.ts +30 -20
  60. package/dist/next.js +6947 -149
  61. package/dist/next.js.map +1 -1
  62. package/dist/next.mjs +7050 -0
  63. package/dist/next.mjs.map +1 -0
  64. package/dist/plugin.d.mts +90 -0
  65. package/dist/plugin.d.ts +90 -0
  66. package/dist/plugin.js +185 -0
  67. package/dist/plugin.js.map +1 -0
  68. package/dist/plugin.mjs +174 -0
  69. package/dist/plugin.mjs.map +1 -0
  70. package/dist/pluginRegistry.d.mts +83 -0
  71. package/dist/pluginRegistry.d.ts +83 -0
  72. package/dist/pluginRegistry.js +303 -0
  73. package/dist/pluginRegistry.js.map +1 -0
  74. package/dist/pluginRegistry.mjs +298 -0
  75. package/dist/pluginRegistry.mjs.map +1 -0
  76. package/dist/{preset.d.cts → preset.d.mts} +29 -2
  77. package/dist/preset.d.ts +29 -2
  78. package/dist/preset.js +318 -21
  79. package/dist/preset.js.map +1 -1
  80. package/dist/preset.mjs +414 -0
  81. package/dist/preset.mjs.map +1 -0
  82. package/dist/rspack.d.mts +33 -0
  83. package/dist/rspack.d.ts +33 -0
  84. package/dist/rspack.js +55 -0
  85. package/dist/rspack.js.map +1 -0
  86. package/dist/rspack.mjs +45 -0
  87. package/dist/rspack.mjs.map +1 -0
  88. package/dist/runtime.d.mts +62 -0
  89. package/dist/runtime.d.ts +62 -0
  90. package/dist/runtime.js +207 -0
  91. package/dist/runtime.js.map +1 -0
  92. package/dist/runtime.mjs +188 -0
  93. package/dist/runtime.mjs.map +1 -0
  94. package/dist/runtimeCss.d.mts +65 -0
  95. package/dist/runtimeCss.d.ts +65 -0
  96. package/dist/runtimeCss.js +188 -0
  97. package/dist/runtimeCss.js.map +1 -0
  98. package/dist/runtimeCss.mjs +173 -0
  99. package/dist/runtimeCss.mjs.map +1 -0
  100. package/dist/scanner.d.mts +25 -0
  101. package/dist/scanner.d.ts +25 -0
  102. package/dist/scanner.js +717 -0
  103. package/dist/scanner.js.map +1 -0
  104. package/dist/scanner.mjs +703 -0
  105. package/dist/scanner.mjs.map +1 -0
  106. package/dist/shared.d.mts +85 -0
  107. package/dist/shared.d.ts +85 -0
  108. package/dist/shared.js +255 -0
  109. package/dist/shared.js.map +1 -0
  110. package/dist/shared.mjs +233 -0
  111. package/dist/shared.mjs.map +1 -0
  112. package/dist/storybookAddon.d.mts +108 -0
  113. package/dist/storybookAddon.d.ts +108 -0
  114. package/dist/storybookAddon.js +95 -0
  115. package/dist/storybookAddon.js.map +1 -0
  116. package/dist/storybookAddon.mjs +88 -0
  117. package/dist/storybookAddon.mjs.map +1 -0
  118. package/dist/svelte.d.mts +114 -0
  119. package/dist/svelte.d.ts +114 -0
  120. package/dist/svelte.js +67 -0
  121. package/dist/svelte.js.map +1 -0
  122. package/dist/svelte.mjs +59 -0
  123. package/dist/svelte.mjs.map +1 -0
  124. package/dist/testing.d.mts +185 -0
  125. package/dist/testing.d.ts +185 -0
  126. package/dist/testing.js +173 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/testing.mjs +158 -0
  129. package/dist/testing.mjs.map +1 -0
  130. package/dist/{theme.d.cts → theme.d.mts} +18 -11
  131. package/dist/theme.d.ts +18 -11
  132. package/dist/theme.js +205 -19
  133. package/dist/theme.js.map +1 -1
  134. package/dist/theme.mjs +311 -0
  135. package/dist/theme.mjs.map +1 -0
  136. package/dist/types-DXr2PmGP.d.mts +31 -0
  137. package/dist/types-DXr2PmGP.d.ts +31 -0
  138. package/dist/vite.d.mts +51 -0
  139. package/dist/vite.d.ts +35 -6
  140. package/dist/vite.js +4254 -57
  141. package/dist/vite.js.map +1 -1
  142. package/dist/vite.mjs +4281 -0
  143. package/dist/vite.mjs.map +1 -0
  144. package/dist/vue.d.mts +89 -0
  145. package/dist/vue.d.ts +89 -0
  146. package/dist/vue.js +104 -0
  147. package/dist/vue.js.map +1 -0
  148. package/dist/vue.mjs +96 -0
  149. package/dist/vue.mjs.map +1 -0
  150. package/package.json +173 -67
  151. package/dist/animate.cjs +0 -252
  152. package/dist/animate.cjs.map +0 -1
  153. package/dist/animate.d.cts +0 -117
  154. package/dist/astTransform-ua-eapqs.d.cts +0 -41
  155. package/dist/astTransform-ua-eapqs.d.ts +0 -41
  156. package/dist/compiler.cjs.map +0 -1
  157. package/dist/css.cjs +0 -71
  158. package/dist/css.cjs.map +0 -1
  159. package/dist/css.d.cts +0 -45
  160. package/dist/css.d.ts +0 -45
  161. package/dist/css.js +0 -62
  162. package/dist/css.js.map +0 -1
  163. package/dist/devtools.cjs.map +0 -1
  164. package/dist/index.cjs +0 -1058
  165. package/dist/index.cjs.map +0 -1
  166. package/dist/next.cjs +0 -268
  167. package/dist/next.cjs.map +0 -1
  168. package/dist/next.d.cts +0 -45
  169. package/dist/plugins.cjs +0 -396
  170. package/dist/plugins.cjs.map +0 -1
  171. package/dist/plugins.d.cts +0 -231
  172. package/dist/plugins.d.ts +0 -231
  173. package/dist/plugins.js +0 -381
  174. package/dist/plugins.js.map +0 -1
  175. package/dist/preset.cjs +0 -129
  176. package/dist/preset.cjs.map +0 -1
  177. package/dist/theme.cjs +0 -154
  178. package/dist/theme.cjs.map +0 -1
  179. package/dist/turbopackLoader.cjs +0 -2689
  180. package/dist/turbopackLoader.cjs.map +0 -1
  181. package/dist/turbopackLoader.d.cts +0 -22
  182. package/dist/turbopackLoader.d.ts +0 -22
  183. package/dist/turbopackLoader.js +0 -2681
  184. package/dist/turbopackLoader.js.map +0 -1
  185. package/dist/vite.cjs +0 -105
  186. package/dist/vite.cjs.map +0 -1
  187. package/dist/vite.d.cts +0 -22
  188. package/dist/webpackLoader.cjs +0 -2670
  189. package/dist/webpackLoader.cjs.map +0 -1
  190. package/dist/webpackLoader.d.cts +0 -24
  191. package/dist/webpackLoader.d.ts +0 -24
  192. package/dist/webpackLoader.js +0 -2662
  193. package/dist/webpackLoader.js.map +0 -1
  194. /package/dist/{devtools.d.cts → devtools.d.mts} +0 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * tailwind-styled-v4 — Testing Utilities
3
+ *
4
+ * Integrasi dengan Jest / Vitest untuk test komponen tw().
5
+ *
6
+ * @example
7
+ * // vitest.config.ts
8
+ * import { tailwindStyledSetup } from '@tailwind-styled/testing'
9
+ * export default defineConfig({ test: { setupFiles: ['@tailwind-styled/testing/setup'] } })
10
+ *
11
+ * // Button.test.ts
12
+ * import { render } from '@testing-library/react'
13
+ * import { expectClasses, getVariantClass } from '@tailwind-styled/testing'
14
+ *
15
+ * test('Button renders primary variant', () => {
16
+ * const { container } = render(<Button intent="primary" />)
17
+ * expectClasses(container.firstChild, ['bg-blue-500', 'text-white'])
18
+ * })
19
+ */
20
+ /**
21
+ * Custom matcher: toHaveClass
22
+ * Dipakai langsung atau via expect.extend(tailwindMatchers)
23
+ *
24
+ * @example
25
+ * expect(element).toHaveClass('bg-blue-500')
26
+ */
27
+ declare function toHaveClass(className: string): (value: {
28
+ classList?: {
29
+ contains: (name: string) => boolean;
30
+ };
31
+ }) => {
32
+ pass: boolean;
33
+ message: () => string;
34
+ };
35
+ /**
36
+ * Custom matcher: toHaveClasses
37
+ * Cek beberapa class sekaligus
38
+ *
39
+ * @example
40
+ * expect(element).toHaveClasses(['px-4', 'py-2', 'rounded'])
41
+ */
42
+ declare function toHaveClasses(classNames: string[]): (value: Element | null | undefined) => {
43
+ pass: boolean;
44
+ message: () => string;
45
+ };
46
+ /**
47
+ * Custom matcher: toNotHaveClass
48
+ *
49
+ * @example
50
+ * expect(element).toNotHaveClass('hidden')
51
+ */
52
+ declare function toNotHaveClass(className: string): (value: Element | null | undefined) => {
53
+ pass: boolean;
54
+ message: () => string;
55
+ };
56
+ /** Semua matchers — pakai dengan expect.extend(tailwindMatchers) */
57
+ declare const tailwindMatchers: {
58
+ toHaveClass: typeof toHaveClass;
59
+ toHaveClasses: typeof toHaveClasses;
60
+ toNotHaveClass: typeof toNotHaveClass;
61
+ };
62
+ /**
63
+ * Assert element punya semua class yang diharapkan.
64
+ * Lebih ergonomis dari `expect(el).toHaveClasses([...])` untuk banyak class.
65
+ *
66
+ * @example
67
+ * const { container } = render(<Button intent="primary" size="lg" />)
68
+ * expectClasses(container.firstChild, ['bg-blue-500', 'text-white', 'h-12'])
69
+ */
70
+ declare function expectClasses(element: Element | null | undefined, classes: string[]): void;
71
+ /**
72
+ * Assert element tidak punya class tertentu.
73
+ *
74
+ * @example
75
+ * expectNoClasses(container.firstChild, ['opacity-50', 'cursor-not-allowed'])
76
+ */
77
+ declare function expectNoClasses(element: Element | null | undefined, classes: string[]): void;
78
+ /**
79
+ * Extract class list dari element sebagai sorted array.
80
+ * Berguna untuk snapshot testing.
81
+ *
82
+ * @example
83
+ * expect(getClassList(element)).toMatchSnapshot()
84
+ */
85
+ declare function getClassList(element: Element | null | undefined): string[];
86
+ /**
87
+ * Buat snapshot dari semua kombinasi variant.
88
+ * Berguna untuk regression testing pada styled components.
89
+ *
90
+ * @example
91
+ * const buttonVariants = snapshotVariants(
92
+ * (props) => {
93
+ * const { container } = render(<Button {...props} />)
94
+ * return container.firstChild?.className ?? ''
95
+ * },
96
+ * [
97
+ * { intent: 'primary', size: 'sm' },
98
+ * { intent: 'primary', size: 'lg' },
99
+ * { intent: 'danger', size: 'sm' },
100
+ * ]
101
+ * )
102
+ * expect(buttonVariants).toMatchSnapshot()
103
+ */
104
+ declare function snapshotVariants<T>(render: (variant: T) => string, variants: T[]): {
105
+ variant: T;
106
+ output: string;
107
+ }[];
108
+ /**
109
+ * Generate semua kombinasi dari variant matrix.
110
+ *
111
+ * @example
112
+ * const combinations = expandVariantMatrix({
113
+ * intent: ['primary', 'danger'],
114
+ * size: ['sm', 'md', 'lg'],
115
+ * disabled: [true, false],
116
+ * })
117
+ * // → 2 × 3 × 2 = 12 kombinasi
118
+ */
119
+ declare function expandVariantMatrix(matrix: Record<string, Array<string | number | boolean>>): Array<Record<string, string | number | boolean>>;
120
+ /**
121
+ * Test semua kombinasi variant sekaligus — no missing coverage.
122
+ *
123
+ * @example
124
+ * testAllVariants(
125
+ * Button,
126
+ * { intent: ['primary','danger'], size: ['sm','lg'] },
127
+ * (el, variant) => {
128
+ * expect(el).not.toBeNull()
129
+ * if (variant.intent === 'primary') expectClasses(el, ['bg-blue-500'])
130
+ * }
131
+ * )
132
+ */
133
+ declare function testAllVariants(matrix: Record<string, Array<string | number | boolean>>, testFn: (variant: Record<string, string | number | boolean>) => void): void;
134
+ /**
135
+ * Bandingkan dua class string secara semantik (urutan tidak penting).
136
+ *
137
+ * @example
138
+ * expectClassesEqual('px-4 py-2 bg-blue-500', 'bg-blue-500 px-4 py-2') // pass
139
+ */
140
+ declare function expectClassesEqual(actual: string, expected: string): void;
141
+ interface EngineMetricsSnapshot {
142
+ totalFiles?: number;
143
+ uniqueClasses?: number;
144
+ buildTimeMs?: number;
145
+ cssBytes?: number;
146
+ cacheHits?: number;
147
+ cacheMisses?: number;
148
+ incrementalRuns?: number;
149
+ fullRescans?: number;
150
+ }
151
+ /**
152
+ * Assert engine metrics snapshot meets minimum thresholds.
153
+ *
154
+ * @example
155
+ * const result = await engine.build()
156
+ * expectEngineMetrics(metrics.snapshot(), {
157
+ * totalFiles: 10,
158
+ * buildTimeMs: 5000, // max
159
+ * })
160
+ */
161
+ declare function expectEngineMetrics(metrics: EngineMetricsSnapshot, expectations: {
162
+ minFiles?: number;
163
+ maxBuildTimeMs?: number;
164
+ minUniqueClasses?: number;
165
+ cacheHitRateMin?: number;
166
+ }): void;
167
+ /**
168
+ * Custom Jest/Vitest matcher: toHaveEngineMetrics
169
+ *
170
+ * @example
171
+ * expect(metrics).toHaveEngineMetrics({ minFiles: 1 })
172
+ */
173
+ declare function toHaveEngineMetrics(expectations: Parameters<typeof expectEngineMetrics>[1]): (metrics: EngineMetricsSnapshot) => {
174
+ pass: boolean;
175
+ message: () => string;
176
+ };
177
+ /** All matchers including engine metrics */
178
+ declare const tailwindMatchersWithMetrics: {
179
+ toHaveClass: typeof toHaveClass;
180
+ toHaveClasses: typeof toHaveClasses;
181
+ toNotHaveClass: typeof toNotHaveClass;
182
+ toHaveEngineMetrics: typeof toHaveEngineMetrics;
183
+ };
184
+
185
+ export { type EngineMetricsSnapshot, expandVariantMatrix, expectClasses, expectClassesEqual, expectEngineMetrics, expectNoClasses, getClassList, snapshotVariants, tailwindMatchers, tailwindMatchersWithMetrics, testAllVariants, toHaveClass, toHaveClasses, toHaveEngineMetrics, toNotHaveClass };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * tailwind-styled-v4 — Testing Utilities
3
+ *
4
+ * Integrasi dengan Jest / Vitest untuk test komponen tw().
5
+ *
6
+ * @example
7
+ * // vitest.config.ts
8
+ * import { tailwindStyledSetup } from '@tailwind-styled/testing'
9
+ * export default defineConfig({ test: { setupFiles: ['@tailwind-styled/testing/setup'] } })
10
+ *
11
+ * // Button.test.ts
12
+ * import { render } from '@testing-library/react'
13
+ * import { expectClasses, getVariantClass } from '@tailwind-styled/testing'
14
+ *
15
+ * test('Button renders primary variant', () => {
16
+ * const { container } = render(<Button intent="primary" />)
17
+ * expectClasses(container.firstChild, ['bg-blue-500', 'text-white'])
18
+ * })
19
+ */
20
+ /**
21
+ * Custom matcher: toHaveClass
22
+ * Dipakai langsung atau via expect.extend(tailwindMatchers)
23
+ *
24
+ * @example
25
+ * expect(element).toHaveClass('bg-blue-500')
26
+ */
27
+ declare function toHaveClass(className: string): (value: {
28
+ classList?: {
29
+ contains: (name: string) => boolean;
30
+ };
31
+ }) => {
32
+ pass: boolean;
33
+ message: () => string;
34
+ };
35
+ /**
36
+ * Custom matcher: toHaveClasses
37
+ * Cek beberapa class sekaligus
38
+ *
39
+ * @example
40
+ * expect(element).toHaveClasses(['px-4', 'py-2', 'rounded'])
41
+ */
42
+ declare function toHaveClasses(classNames: string[]): (value: Element | null | undefined) => {
43
+ pass: boolean;
44
+ message: () => string;
45
+ };
46
+ /**
47
+ * Custom matcher: toNotHaveClass
48
+ *
49
+ * @example
50
+ * expect(element).toNotHaveClass('hidden')
51
+ */
52
+ declare function toNotHaveClass(className: string): (value: Element | null | undefined) => {
53
+ pass: boolean;
54
+ message: () => string;
55
+ };
56
+ /** Semua matchers — pakai dengan expect.extend(tailwindMatchers) */
57
+ declare const tailwindMatchers: {
58
+ toHaveClass: typeof toHaveClass;
59
+ toHaveClasses: typeof toHaveClasses;
60
+ toNotHaveClass: typeof toNotHaveClass;
61
+ };
62
+ /**
63
+ * Assert element punya semua class yang diharapkan.
64
+ * Lebih ergonomis dari `expect(el).toHaveClasses([...])` untuk banyak class.
65
+ *
66
+ * @example
67
+ * const { container } = render(<Button intent="primary" size="lg" />)
68
+ * expectClasses(container.firstChild, ['bg-blue-500', 'text-white', 'h-12'])
69
+ */
70
+ declare function expectClasses(element: Element | null | undefined, classes: string[]): void;
71
+ /**
72
+ * Assert element tidak punya class tertentu.
73
+ *
74
+ * @example
75
+ * expectNoClasses(container.firstChild, ['opacity-50', 'cursor-not-allowed'])
76
+ */
77
+ declare function expectNoClasses(element: Element | null | undefined, classes: string[]): void;
78
+ /**
79
+ * Extract class list dari element sebagai sorted array.
80
+ * Berguna untuk snapshot testing.
81
+ *
82
+ * @example
83
+ * expect(getClassList(element)).toMatchSnapshot()
84
+ */
85
+ declare function getClassList(element: Element | null | undefined): string[];
86
+ /**
87
+ * Buat snapshot dari semua kombinasi variant.
88
+ * Berguna untuk regression testing pada styled components.
89
+ *
90
+ * @example
91
+ * const buttonVariants = snapshotVariants(
92
+ * (props) => {
93
+ * const { container } = render(<Button {...props} />)
94
+ * return container.firstChild?.className ?? ''
95
+ * },
96
+ * [
97
+ * { intent: 'primary', size: 'sm' },
98
+ * { intent: 'primary', size: 'lg' },
99
+ * { intent: 'danger', size: 'sm' },
100
+ * ]
101
+ * )
102
+ * expect(buttonVariants).toMatchSnapshot()
103
+ */
104
+ declare function snapshotVariants<T>(render: (variant: T) => string, variants: T[]): {
105
+ variant: T;
106
+ output: string;
107
+ }[];
108
+ /**
109
+ * Generate semua kombinasi dari variant matrix.
110
+ *
111
+ * @example
112
+ * const combinations = expandVariantMatrix({
113
+ * intent: ['primary', 'danger'],
114
+ * size: ['sm', 'md', 'lg'],
115
+ * disabled: [true, false],
116
+ * })
117
+ * // → 2 × 3 × 2 = 12 kombinasi
118
+ */
119
+ declare function expandVariantMatrix(matrix: Record<string, Array<string | number | boolean>>): Array<Record<string, string | number | boolean>>;
120
+ /**
121
+ * Test semua kombinasi variant sekaligus — no missing coverage.
122
+ *
123
+ * @example
124
+ * testAllVariants(
125
+ * Button,
126
+ * { intent: ['primary','danger'], size: ['sm','lg'] },
127
+ * (el, variant) => {
128
+ * expect(el).not.toBeNull()
129
+ * if (variant.intent === 'primary') expectClasses(el, ['bg-blue-500'])
130
+ * }
131
+ * )
132
+ */
133
+ declare function testAllVariants(matrix: Record<string, Array<string | number | boolean>>, testFn: (variant: Record<string, string | number | boolean>) => void): void;
134
+ /**
135
+ * Bandingkan dua class string secara semantik (urutan tidak penting).
136
+ *
137
+ * @example
138
+ * expectClassesEqual('px-4 py-2 bg-blue-500', 'bg-blue-500 px-4 py-2') // pass
139
+ */
140
+ declare function expectClassesEqual(actual: string, expected: string): void;
141
+ interface EngineMetricsSnapshot {
142
+ totalFiles?: number;
143
+ uniqueClasses?: number;
144
+ buildTimeMs?: number;
145
+ cssBytes?: number;
146
+ cacheHits?: number;
147
+ cacheMisses?: number;
148
+ incrementalRuns?: number;
149
+ fullRescans?: number;
150
+ }
151
+ /**
152
+ * Assert engine metrics snapshot meets minimum thresholds.
153
+ *
154
+ * @example
155
+ * const result = await engine.build()
156
+ * expectEngineMetrics(metrics.snapshot(), {
157
+ * totalFiles: 10,
158
+ * buildTimeMs: 5000, // max
159
+ * })
160
+ */
161
+ declare function expectEngineMetrics(metrics: EngineMetricsSnapshot, expectations: {
162
+ minFiles?: number;
163
+ maxBuildTimeMs?: number;
164
+ minUniqueClasses?: number;
165
+ cacheHitRateMin?: number;
166
+ }): void;
167
+ /**
168
+ * Custom Jest/Vitest matcher: toHaveEngineMetrics
169
+ *
170
+ * @example
171
+ * expect(metrics).toHaveEngineMetrics({ minFiles: 1 })
172
+ */
173
+ declare function toHaveEngineMetrics(expectations: Parameters<typeof expectEngineMetrics>[1]): (metrics: EngineMetricsSnapshot) => {
174
+ pass: boolean;
175
+ message: () => string;
176
+ };
177
+ /** All matchers including engine metrics */
178
+ declare const tailwindMatchersWithMetrics: {
179
+ toHaveClass: typeof toHaveClass;
180
+ toHaveClasses: typeof toHaveClasses;
181
+ toNotHaveClass: typeof toNotHaveClass;
182
+ toHaveEngineMetrics: typeof toHaveEngineMetrics;
183
+ };
184
+
185
+ export { type EngineMetricsSnapshot, expandVariantMatrix, expectClasses, expectClassesEqual, expectEngineMetrics, expectNoClasses, getClassList, snapshotVariants, tailwindMatchers, tailwindMatchersWithMetrics, testAllVariants, toHaveClass, toHaveClasses, toHaveEngineMetrics, toNotHaveClass };
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ /* tailwind-styled-v4 v5.0.1 | MIT | https://github.com/dictionar32/tailwind-styled-v4 */
4
+
5
+ // packages/testing/src/index.ts
6
+ function toHaveClass(className) {
7
+ return (value) => {
8
+ const pass = Boolean(value?.classList?.contains(className));
9
+ return {
10
+ pass,
11
+ message: () => `expected element ${pass ? "not " : ""}to contain class '${className}'`
12
+ };
13
+ };
14
+ }
15
+ function toHaveClasses(classNames) {
16
+ return (value) => {
17
+ if (!value) return { pass: false, message: () => "element is null or undefined" };
18
+ const missing = classNames.filter((c) => !value.classList.contains(c));
19
+ const pass = missing.length === 0;
20
+ return {
21
+ pass,
22
+ message: () => pass ? `expected element not to have all classes: ${classNames.join(", ")}` : `expected element to have classes: ${missing.join(", ")}`
23
+ };
24
+ };
25
+ }
26
+ function toNotHaveClass(className) {
27
+ return (value) => {
28
+ if (!value) return { pass: false, message: () => "element is null or undefined" };
29
+ const pass = !value.classList.contains(className);
30
+ return {
31
+ pass,
32
+ message: () => `expected element ${pass ? "" : "not "}to have class '${className}'`
33
+ };
34
+ };
35
+ }
36
+ var tailwindMatchers = { toHaveClass, toHaveClasses, toNotHaveClass };
37
+ function expectClasses(element, classes) {
38
+ if (!element) throw new Error("expectClasses: element is null or undefined");
39
+ const missing = classes.filter((c) => !element.classList.contains(c));
40
+ if (missing.length > 0) {
41
+ throw new Error(
42
+ `Expected element to have classes: ${missing.join(", ")}
43
+ Actual classes: ${element.className}`
44
+ );
45
+ }
46
+ }
47
+ function expectNoClasses(element, classes) {
48
+ if (!element) throw new Error("expectNoClasses: element is null or undefined");
49
+ const found = classes.filter((c) => element.classList.contains(c));
50
+ if (found.length > 0) {
51
+ throw new Error(
52
+ `Expected element NOT to have classes: ${found.join(", ")}
53
+ Actual classes: ${element.className}`
54
+ );
55
+ }
56
+ }
57
+ function getClassList(element) {
58
+ if (!element) return [];
59
+ return Array.from(element.classList).sort();
60
+ }
61
+ function snapshotVariants(render, variants) {
62
+ return variants.map((variant) => ({
63
+ variant,
64
+ output: render(variant)
65
+ }));
66
+ }
67
+ function expandVariantMatrix(matrix) {
68
+ const keys = Object.keys(matrix);
69
+ if (keys.length === 0) return [{}];
70
+ const result = [];
71
+ function walk(index, current) {
72
+ if (index >= keys.length) {
73
+ result.push({ ...current });
74
+ return;
75
+ }
76
+ const key = keys[index];
77
+ for (const value of matrix[key] ?? []) {
78
+ current[key] = value;
79
+ walk(index + 1, current);
80
+ }
81
+ }
82
+ walk(0, {});
83
+ return result;
84
+ }
85
+ function testAllVariants(matrix, testFn) {
86
+ const combinations = expandVariantMatrix(matrix);
87
+ for (const variant of combinations) {
88
+ testFn(variant);
89
+ }
90
+ }
91
+ function expectClassesEqual(actual, expected) {
92
+ const actualSet = new Set(actual.trim().split(/\s+/).filter(Boolean));
93
+ const expectedSet = new Set(expected.trim().split(/\s+/).filter(Boolean));
94
+ const missing = [...expectedSet].filter((c) => !actualSet.has(c));
95
+ const extra = [...actualSet].filter((c) => !expectedSet.has(c));
96
+ if (missing.length > 0 || extra.length > 0) {
97
+ const parts = [];
98
+ if (missing.length > 0) parts.push(`Missing: ${missing.join(", ")}`);
99
+ if (extra.length > 0) parts.push(`Extra: ${extra.join(", ")}`);
100
+ throw new Error(`Class mismatch:
101
+ ${parts.join("\n ")}`);
102
+ }
103
+ }
104
+ function expectEngineMetrics(metrics, expectations) {
105
+ if (expectations.minFiles !== void 0) {
106
+ const actual = metrics.totalFiles ?? 0;
107
+ if (actual < expectations.minFiles) {
108
+ throw new Error(
109
+ `Engine metrics: expected at least ${expectations.minFiles} files, got ${actual}`
110
+ );
111
+ }
112
+ }
113
+ if (expectations.maxBuildTimeMs !== void 0) {
114
+ const actual = metrics.buildTimeMs ?? 0;
115
+ if (actual > expectations.maxBuildTimeMs) {
116
+ throw new Error(
117
+ `Engine metrics: build took ${actual}ms, expected \u2264 ${expectations.maxBuildTimeMs}ms`
118
+ );
119
+ }
120
+ }
121
+ if (expectations.minUniqueClasses !== void 0) {
122
+ const actual = metrics.uniqueClasses ?? 0;
123
+ if (actual < expectations.minUniqueClasses) {
124
+ throw new Error(
125
+ `Engine metrics: expected at least ${expectations.minUniqueClasses} unique classes, got ${actual}`
126
+ );
127
+ }
128
+ }
129
+ if (expectations.cacheHitRateMin !== void 0) {
130
+ const hits = metrics.cacheHits ?? 0;
131
+ const total = hits + (metrics.cacheMisses ?? 0);
132
+ const rate = total > 0 ? hits / total : 0;
133
+ if (rate < expectations.cacheHitRateMin) {
134
+ throw new Error(
135
+ `Engine metrics: cache hit rate ${(rate * 100).toFixed(1)}%, expected \u2265 ${(expectations.cacheHitRateMin * 100).toFixed(1)}%`
136
+ );
137
+ }
138
+ }
139
+ }
140
+ function toHaveEngineMetrics(expectations) {
141
+ return (metrics) => {
142
+ try {
143
+ expectEngineMetrics(metrics, expectations);
144
+ return { pass: true, message: () => "engine metrics matched expectations" };
145
+ } catch (e) {
146
+ const msg = e instanceof Error ? e.message : String(e);
147
+ return { pass: false, message: () => msg };
148
+ }
149
+ };
150
+ }
151
+ var tailwindMatchersWithMetrics = {
152
+ toHaveClass,
153
+ toHaveClasses,
154
+ toNotHaveClass,
155
+ toHaveEngineMetrics
156
+ };
157
+
158
+ exports.expandVariantMatrix = expandVariantMatrix;
159
+ exports.expectClasses = expectClasses;
160
+ exports.expectClassesEqual = expectClassesEqual;
161
+ exports.expectEngineMetrics = expectEngineMetrics;
162
+ exports.expectNoClasses = expectNoClasses;
163
+ exports.getClassList = getClassList;
164
+ exports.snapshotVariants = snapshotVariants;
165
+ exports.tailwindMatchers = tailwindMatchers;
166
+ exports.tailwindMatchersWithMetrics = tailwindMatchersWithMetrics;
167
+ exports.testAllVariants = testAllVariants;
168
+ exports.toHaveClass = toHaveClass;
169
+ exports.toHaveClasses = toHaveClasses;
170
+ exports.toHaveEngineMetrics = toHaveEngineMetrics;
171
+ exports.toNotHaveClass = toNotHaveClass;
172
+ //# sourceMappingURL=testing.js.map
173
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../packages/testing/src/index.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["/**\n * tailwind-styled-v4 — Testing Utilities\n *\n * Integrasi dengan Jest / Vitest untuk test komponen tw().\n *\n * @example\n * // vitest.config.ts\n * import { tailwindStyledSetup } from '@tailwind-styled/testing'\n * export default defineConfig({ test: { setupFiles: ['@tailwind-styled/testing/setup'] } })\n *\n * // Button.test.ts\n * import { render } from '@testing-library/react'\n * import { expectClasses, getVariantClass } from '@tailwind-styled/testing'\n *\n * test('Button renders primary variant', () => {\n * const { container } = render(<Button intent=\"primary\" />)\n * expectClasses(container.firstChild, ['bg-blue-500', 'text-white'])\n * })\n */\n\n// ─── Jest/Vitest custom matchers ─────────────────────────────────────────────\n\n/**\n * Custom matcher: toHaveClass\n * Dipakai langsung atau via expect.extend(tailwindMatchers)\n *\n * @example\n * expect(element).toHaveClass('bg-blue-500')\n */\nexport function toHaveClass(className: string) {\n return (value: { classList?: { contains: (name: string) => boolean } }) => {\n const pass = Boolean(value?.classList?.contains(className))\n return {\n pass,\n message: () => `expected element ${pass ? \"not \" : \"\"}to contain class '${className}'`,\n }\n }\n}\n\n/**\n * Custom matcher: toHaveClasses\n * Cek beberapa class sekaligus\n *\n * @example\n * expect(element).toHaveClasses(['px-4', 'py-2', 'rounded'])\n */\nexport function toHaveClasses(classNames: string[]) {\n return (value: Element | null | undefined) => {\n if (!value) return { pass: false, message: () => \"element is null or undefined\" }\n const missing = classNames.filter((c) => !value.classList.contains(c))\n const pass = missing.length === 0\n return {\n pass,\n message: () =>\n pass\n ? `expected element not to have all classes: ${classNames.join(\", \")}`\n : `expected element to have classes: ${missing.join(\", \")}`,\n }\n }\n}\n\n/**\n * Custom matcher: toNotHaveClass\n *\n * @example\n * expect(element).toNotHaveClass('hidden')\n */\nexport function toNotHaveClass(className: string) {\n return (value: Element | null | undefined) => {\n if (!value) return { pass: false, message: () => \"element is null or undefined\" }\n const pass = !value.classList.contains(className)\n return {\n pass,\n message: () => `expected element ${pass ? \"\" : \"not \"}to have class '${className}'`,\n }\n }\n}\n\n/** Semua matchers — pakai dengan expect.extend(tailwindMatchers) */\nexport const tailwindMatchers = { toHaveClass, toHaveClasses, toNotHaveClass }\n\n// ─── Helper utilities ────────────────────────────────────────────────────────\n\n/**\n * Assert element punya semua class yang diharapkan.\n * Lebih ergonomis dari `expect(el).toHaveClasses([...])` untuk banyak class.\n *\n * @example\n * const { container } = render(<Button intent=\"primary\" size=\"lg\" />)\n * expectClasses(container.firstChild, ['bg-blue-500', 'text-white', 'h-12'])\n */\nexport function expectClasses(element: Element | null | undefined, classes: string[]): void {\n if (!element) throw new Error(\"expectClasses: element is null or undefined\")\n const missing = classes.filter((c) => !element.classList.contains(c))\n if (missing.length > 0) {\n throw new Error(\n `Expected element to have classes: ${missing.join(\", \")}\\n` +\n ` Actual classes: ${element.className}`\n )\n }\n}\n\n/**\n * Assert element tidak punya class tertentu.\n *\n * @example\n * expectNoClasses(container.firstChild, ['opacity-50', 'cursor-not-allowed'])\n */\nexport function expectNoClasses(element: Element | null | undefined, classes: string[]): void {\n if (!element) throw new Error(\"expectNoClasses: element is null or undefined\")\n const found = classes.filter((c) => element.classList.contains(c))\n if (found.length > 0) {\n throw new Error(\n `Expected element NOT to have classes: ${found.join(\", \")}\\n` +\n ` Actual classes: ${element.className}`\n )\n }\n}\n\n/**\n * Extract class list dari element sebagai sorted array.\n * Berguna untuk snapshot testing.\n *\n * @example\n * expect(getClassList(element)).toMatchSnapshot()\n */\nexport function getClassList(element: Element | null | undefined): string[] {\n if (!element) return []\n return Array.from(element.classList).sort()\n}\n\n// ─── Variant snapshot helpers ────────────────────────────────────────────────\n\n/**\n * Buat snapshot dari semua kombinasi variant.\n * Berguna untuk regression testing pada styled components.\n *\n * @example\n * const buttonVariants = snapshotVariants(\n * (props) => {\n * const { container } = render(<Button {...props} />)\n * return container.firstChild?.className ?? ''\n * },\n * [\n * { intent: 'primary', size: 'sm' },\n * { intent: 'primary', size: 'lg' },\n * { intent: 'danger', size: 'sm' },\n * ]\n * )\n * expect(buttonVariants).toMatchSnapshot()\n */\nexport function snapshotVariants<T>(render: (variant: T) => string, variants: T[]) {\n return variants.map((variant) => ({\n variant,\n output: render(variant),\n }))\n}\n\n/**\n * Generate semua kombinasi dari variant matrix.\n *\n * @example\n * const combinations = expandVariantMatrix({\n * intent: ['primary', 'danger'],\n * size: ['sm', 'md', 'lg'],\n * disabled: [true, false],\n * })\n * // → 2 × 3 × 2 = 12 kombinasi\n */\nexport function expandVariantMatrix(\n matrix: Record<string, Array<string | number | boolean>>\n): Array<Record<string, string | number | boolean>> {\n const keys = Object.keys(matrix)\n if (keys.length === 0) return [{}]\n\n const result: Array<Record<string, string | number | boolean>> = []\n\n function walk(index: number, current: Record<string, string | number | boolean>) {\n if (index >= keys.length) {\n result.push({ ...current })\n return\n }\n const key = keys[index]!\n for (const value of matrix[key] ?? []) {\n current[key] = value\n walk(index + 1, current)\n }\n }\n\n walk(0, {})\n return result\n}\n\n/**\n * Test semua kombinasi variant sekaligus — no missing coverage.\n *\n * @example\n * testAllVariants(\n * Button,\n * { intent: ['primary','danger'], size: ['sm','lg'] },\n * (el, variant) => {\n * expect(el).not.toBeNull()\n * if (variant.intent === 'primary') expectClasses(el, ['bg-blue-500'])\n * }\n * )\n */\nexport function testAllVariants(\n matrix: Record<string, Array<string | number | boolean>>,\n testFn: (variant: Record<string, string | number | boolean>) => void\n): void {\n const combinations = expandVariantMatrix(matrix)\n for (const variant of combinations) {\n testFn(variant)\n }\n}\n\n// ─── CSS-in-JS output assertions ─────────────────────────────────────────────\n\n/**\n * Bandingkan dua class string secara semantik (urutan tidak penting).\n *\n * @example\n * expectClassesEqual('px-4 py-2 bg-blue-500', 'bg-blue-500 px-4 py-2') // pass\n */\nexport function expectClassesEqual(actual: string, expected: string): void {\n const actualSet = new Set(actual.trim().split(/\\s+/).filter(Boolean))\n const expectedSet = new Set(expected.trim().split(/\\s+/).filter(Boolean))\n\n const missing = [...expectedSet].filter((c) => !actualSet.has(c))\n const extra = [...actualSet].filter((c) => !expectedSet.has(c))\n\n if (missing.length > 0 || extra.length > 0) {\n const parts: string[] = []\n if (missing.length > 0) parts.push(`Missing: ${missing.join(\", \")}`)\n if (extra.length > 0) parts.push(`Extra: ${extra.join(\", \")}`)\n throw new Error(`Class mismatch:\\n ${parts.join(\"\\n \")}`)\n }\n}\n\n// ─── Engine metrics matchers ──────────────────────────────────────────────────\n\nexport interface EngineMetricsSnapshot {\n totalFiles?: number\n uniqueClasses?: number\n buildTimeMs?: number\n cssBytes?: number\n cacheHits?: number\n cacheMisses?: number\n incrementalRuns?: number\n fullRescans?: number\n}\n\n/**\n * Assert engine metrics snapshot meets minimum thresholds.\n *\n * @example\n * const result = await engine.build()\n * expectEngineMetrics(metrics.snapshot(), {\n * totalFiles: 10,\n * buildTimeMs: 5000, // max\n * })\n */\nexport function expectEngineMetrics(\n metrics: EngineMetricsSnapshot,\n expectations: {\n minFiles?: number\n maxBuildTimeMs?: number\n minUniqueClasses?: number\n cacheHitRateMin?: number // 0–1\n }\n): void {\n if (expectations.minFiles !== undefined) {\n const actual = metrics.totalFiles ?? 0\n if (actual < expectations.minFiles) {\n throw new Error(\n `Engine metrics: expected at least ${expectations.minFiles} files, got ${actual}`\n )\n }\n }\n\n if (expectations.maxBuildTimeMs !== undefined) {\n const actual = metrics.buildTimeMs ?? 0\n if (actual > expectations.maxBuildTimeMs) {\n throw new Error(\n `Engine metrics: build took ${actual}ms, expected ≤ ${expectations.maxBuildTimeMs}ms`\n )\n }\n }\n\n if (expectations.minUniqueClasses !== undefined) {\n const actual = metrics.uniqueClasses ?? 0\n if (actual < expectations.minUniqueClasses) {\n throw new Error(\n `Engine metrics: expected at least ${expectations.minUniqueClasses} unique classes, got ${actual}`\n )\n }\n }\n\n if (expectations.cacheHitRateMin !== undefined) {\n const hits = metrics.cacheHits ?? 0\n const total = hits + (metrics.cacheMisses ?? 0)\n const rate = total > 0 ? hits / total : 0\n if (rate < expectations.cacheHitRateMin) {\n throw new Error(\n `Engine metrics: cache hit rate ${(rate * 100).toFixed(1)}%, expected ≥ ${(expectations.cacheHitRateMin * 100).toFixed(1)}%`\n )\n }\n }\n}\n\n/**\n * Custom Jest/Vitest matcher: toHaveEngineMetrics\n *\n * @example\n * expect(metrics).toHaveEngineMetrics({ minFiles: 1 })\n */\nexport function toHaveEngineMetrics(expectations: Parameters<typeof expectEngineMetrics>[1]) {\n return (metrics: EngineMetricsSnapshot) => {\n try {\n expectEngineMetrics(metrics, expectations)\n return { pass: true, message: () => \"engine metrics matched expectations\" }\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e)\n return { pass: false, message: () => msg }\n }\n }\n}\n\n/** All matchers including engine metrics */\nexport const tailwindMatchersWithMetrics = {\n toHaveClass,\n toHaveClasses,\n toNotHaveClass,\n toHaveEngineMetrics,\n}\n"]}