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,249 @@
1
+ 'use strict';
2
+
3
+ var http = require('http');
4
+ var fs = require('fs');
5
+ var path = require('path');
6
+ var events$1 = require('events');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var http__default = /*#__PURE__*/_interopDefault(http);
11
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
12
+ var path__default = /*#__PURE__*/_interopDefault(path);
13
+
14
+ /* tailwind-styled-v4 v5.0.1 | MIT | https://github.com/dictionar32/tailwind-styled-v4 */
15
+
16
+ var port = Number(process.env.PORT ?? 3e3);
17
+ var METRICS_FILE = path__default.default.join(process.cwd(), ".tw-cache", "metrics.json");
18
+ var MAX_HISTORY = 100;
19
+ exports.currentMetrics = {
20
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
21
+ buildMs: null,
22
+ scanMs: null,
23
+ memoryMb: null,
24
+ classCount: null,
25
+ fileCount: null,
26
+ cssBytes: null,
27
+ mode: "idle"
28
+ };
29
+ var history = [];
30
+ var events = new events$1.EventEmitter();
31
+ function updateMetrics(data) {
32
+ exports.currentMetrics = { ...exports.currentMetrics, ...data, generatedAt: (/* @__PURE__ */ new Date()).toISOString() };
33
+ history.push({ ...exports.currentMetrics });
34
+ if (history.length > MAX_HISTORY) history.shift();
35
+ events.emit("update", exports.currentMetrics);
36
+ }
37
+ function watchMetricsFile() {
38
+ const dir = path__default.default.dirname(METRICS_FILE);
39
+ if (!fs__default.default.existsSync(dir)) {
40
+ try {
41
+ fs__default.default.mkdirSync(dir, { recursive: true });
42
+ } catch {
43
+ }
44
+ }
45
+ if (fs__default.default.existsSync(METRICS_FILE)) {
46
+ try {
47
+ const data = JSON.parse(fs__default.default.readFileSync(METRICS_FILE, "utf8"));
48
+ updateMetrics(data);
49
+ } catch {
50
+ }
51
+ }
52
+ try {
53
+ fs__default.default.watch(METRICS_FILE, { persistent: false }, (eventType) => {
54
+ if (eventType === "change") {
55
+ try {
56
+ const data = JSON.parse(fs__default.default.readFileSync(METRICS_FILE, "utf8"));
57
+ updateMetrics(data);
58
+ } catch {
59
+ }
60
+ }
61
+ });
62
+ } catch {
63
+ setInterval(() => {
64
+ if (fs__default.default.existsSync(METRICS_FILE)) {
65
+ try {
66
+ const data = JSON.parse(fs__default.default.readFileSync(METRICS_FILE, "utf8"));
67
+ if (data.generatedAt !== exports.currentMetrics.generatedAt) updateMetrics(data);
68
+ } catch {
69
+ }
70
+ }
71
+ }, 2e3);
72
+ }
73
+ }
74
+ var dashboardHtml = `<!doctype html>
75
+ <html lang="en">
76
+ <head>
77
+ <meta charset="utf-8"/>
78
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
79
+ <title>tailwind-styled dashboard</title>
80
+ <style>
81
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
82
+ :root {
83
+ --bg: #0f1117; --surface: #1a1d2e; --border: #2a2d3e;
84
+ --text: #e2e8f0; --muted: #8892a4; --accent: #38bdf8;
85
+ --green: #4ade80; --amber: #fbbf24; --red: #f87171;
86
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
87
+ }
88
+ body { background: var(--bg); color: var(--text); min-height: 100vh; padding: 2rem }
89
+ h1 { font-size: 1.1rem; color: var(--muted); font-weight: 400; letter-spacing: 0.1em;
90
+ text-transform: uppercase; margin-bottom: 2rem; border-bottom: 1px solid var(--border);
91
+ padding-bottom: 1rem }
92
+ h1 span { color: var(--accent) }
93
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem }
94
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
95
+ padding: 1.25rem; transition: border-color .2s }
96
+ .card:hover { border-color: var(--accent) }
97
+ .card .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .1em;
98
+ color: var(--muted); margin-bottom: .5rem }
99
+ .card .value { font-size: 1.8rem; font-weight: 700; color: var(--text) }
100
+ .card .value.good { color: var(--green) }
101
+ .card .value.warn { color: var(--amber) }
102
+ .card .value.bad { color: var(--red) }
103
+ .card .unit { font-size: .75rem; color: var(--muted); margin-left: .25rem }
104
+ .section-title { font-size: .75rem; text-transform: uppercase; letter-spacing: .1em;
105
+ color: var(--muted); margin: 2rem 0 .75rem }
106
+ .raw { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
107
+ padding: 1rem; font-size: .8rem; overflow-x: auto; white-space: pre; max-height: 300px;
108
+ overflow-y: auto }
109
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green);
110
+ display: inline-block; margin-right: .5rem; animation: pulse 2s infinite }
111
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
112
+ .idle .status-dot { background: var(--muted); animation: none }
113
+ .building .status-dot { background: var(--amber) }
114
+ .error .status-dot { background: var(--red); animation: none }
115
+ #status { font-size: .8rem; color: var(--muted); margin-bottom: 2rem }
116
+ #last-update { font-size: .75rem; color: var(--muted); margin-top: 2rem }
117
+ .history-mini { display: flex; align-items: flex-end; gap: 2px; height: 40px; margin-top: .5rem }
118
+ .history-mini .bar { flex: 1; background: var(--accent); opacity: .4; border-radius: 2px 2px 0 0;
119
+ min-width: 4px; transition: opacity .2s }
120
+ .history-mini .bar:hover { opacity: 1 }
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <h1><span class="status-dot" id="dot"></span>tailwind-styled <span>dashboard</span></h1>
125
+ <div id="status">Connecting...</div>
126
+
127
+ <div class="grid" id="cards">
128
+ <div class="card"><div class="label">Build time</div><div class="value" id="buildMs">\u2014</div></div>
129
+ <div class="card"><div class="label">Scan time</div><div class="value" id="scanMs">\u2014</div></div>
130
+ <div class="card"><div class="label">Classes</div><div class="value" id="classCount">\u2014</div></div>
131
+ <div class="card"><div class="label">Files</div><div class="value" id="fileCount">\u2014</div></div>
132
+ <div class="card"><div class="label">CSS output</div><div class="value" id="cssBytes">\u2014</div></div>
133
+ <div class="card"><div class="label">Memory (heap)</div><div class="value" id="memoryMb">\u2014</div></div>
134
+ </div>
135
+
136
+ <div class="section-title">Build time history</div>
137
+ <div class="card">
138
+ <div class="history-mini" id="history-chart"></div>
139
+ </div>
140
+
141
+ <div class="section-title">Raw metrics</div>
142
+ <div class="raw" id="raw-json">Loading...</div>
143
+ <div id="last-update"></div>
144
+
145
+ <script>
146
+ let prevGenAt = null
147
+
148
+ function fmt(v, unit, warn, bad) {
149
+ if (v == null) return '\u2014'
150
+ const el = document.createElement('span')
151
+ el.textContent = typeof v === 'number' ? v.toFixed(unit === 'ms' ? 0 : 1) : v
152
+ if (unit) el.insertAdjacentHTML('beforeend', '<span class="unit">' + unit + '</span>')
153
+ if (bad != null && v > bad) el.className = 'bad'
154
+ else if (warn != null && v > warn) el.className = 'warn'
155
+ else if (v != null) el.className = 'good'
156
+ return el.outerHTML
157
+ }
158
+
159
+ function fmtBytes(b) {
160
+ if (b == null) return '\u2014'
161
+ if (b < 1024) return b + '<span class="unit">B</span>'
162
+ if (b < 1024*1024) return (b/1024).toFixed(1) + '<span class="unit">KB</span>'
163
+ return (b/1024/1024).toFixed(2) + '<span class="unit">MB</span>'
164
+ }
165
+
166
+ function renderHistory(history) {
167
+ const chart = document.getElementById('history-chart')
168
+ if (!history.length) return
169
+ const vals = history.map(h => h.buildMs ?? 0).filter(v => v > 0)
170
+ if (!vals.length) return
171
+ const max = Math.max(...vals)
172
+ chart.innerHTML = vals.map(v => {
173
+ const h = max > 0 ? Math.max(4, Math.round((v / max) * 40)) : 4
174
+ return '<div class="bar" style="height:' + h + 'px" title="' + v + 'ms"></div>'
175
+ }).join('')
176
+ }
177
+
178
+ async function fetchAndRender() {
179
+ try {
180
+ const [mRes, hRes] = await Promise.all([fetch('/metrics'), fetch('/history')])
181
+ const m = await mRes.json()
182
+ const h = await hRes.json()
183
+
184
+ if (m.generatedAt === prevGenAt) return
185
+ prevGenAt = m.generatedAt
186
+
187
+ document.getElementById('buildMs').innerHTML = fmt(m.buildMs, 'ms', 500, 2000)
188
+ document.getElementById('scanMs').innerHTML = fmt(m.scanMs, 'ms', 200, 1000)
189
+ document.getElementById('classCount').innerHTML = fmt(m.classCount, null, null, null)
190
+ document.getElementById('fileCount').innerHTML = fmt(m.fileCount, null, null, null)
191
+ document.getElementById('cssBytes').innerHTML = fmtBytes(m.cssBytes)
192
+ document.getElementById('memoryMb').innerHTML = fmt(m.memoryMb?.heapUsed, 'MB', 100, 500)
193
+ document.getElementById('raw-json').textContent = JSON.stringify(m, null, 2)
194
+ document.getElementById('last-update').textContent = 'Last update: ' + new Date(m.generatedAt).toLocaleTimeString()
195
+ document.getElementById('status').textContent = 'Mode: ' + (m.mode ?? 'idle')
196
+
197
+ const dot = document.getElementById('dot')
198
+ dot.parentElement.className = m.mode ?? 'idle'
199
+
200
+ renderHistory(h)
201
+ } catch (e) {
202
+ document.getElementById('status').textContent = 'Error: ' + e.message
203
+ }
204
+ }
205
+
206
+ fetchAndRender()
207
+ setInterval(fetchAndRender, 1500)
208
+ </script>
209
+ </body>
210
+ </html>`;
211
+ var server = http__default.default.createServer((req, res) => {
212
+ const url = new URL(req.url, `http://localhost:${port}`);
213
+ if (url.pathname === "/health") {
214
+ res.setHeader("content-type", "application/json");
215
+ res.end(JSON.stringify({ ok: true }));
216
+ return;
217
+ }
218
+ if (url.pathname === "/metrics") {
219
+ res.setHeader("content-type", "application/json");
220
+ res.setHeader("cache-control", "no-cache");
221
+ res.end(JSON.stringify(exports.currentMetrics, null, 2));
222
+ return;
223
+ }
224
+ if (url.pathname === "/history") {
225
+ res.setHeader("content-type", "application/json");
226
+ res.end(JSON.stringify(history));
227
+ return;
228
+ }
229
+ if (url.pathname === "/reset" && req.method === "POST") {
230
+ history.length = 0;
231
+ res.setHeader("content-type", "application/json");
232
+ res.end(JSON.stringify({ ok: true, message: "History cleared" }));
233
+ return;
234
+ }
235
+ res.setHeader("content-type", "text/html; charset=utf-8");
236
+ res.end(dashboardHtml);
237
+ });
238
+ watchMetricsFile();
239
+ server.listen(port, () => {
240
+ console.log(`[tailwind-styled] Dashboard: http://localhost:${port}`);
241
+ console.log(`[tailwind-styled] Metrics: http://localhost:${port}/metrics`);
242
+ console.log(`[tailwind-styled] Watching: ${METRICS_FILE}`);
243
+ });
244
+
245
+ exports.events = events;
246
+ exports.history = history;
247
+ exports.updateMetrics = updateMetrics;
248
+ //# sourceMappingURL=dashboard.js.map
249
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../packages/dashboard/src/server.mjs"],"names":["path","currentMetrics","EventEmitter","fs","http"],"mappings":"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","file":"dashboard.js","sourcesContent":["/**\n * tailwind-styled-v4 Dashboard Server\n *\n * HTTP server yang expose LIVE build metrics dari engine.\n * Bukan lagi hardcoded — connect ke engine via file-based IPC atau\n * event emitter jika dijalankan dalam proses yang sama.\n *\n * Port default: 3000 (override via PORT env var)\n *\n * Endpoints:\n * GET / → HTML dashboard UI\n * GET /metrics → JSON metrics snapshot (live)\n * GET /history → JSON array dari metrics snapshots (max 100)\n * POST /reset → Reset history\n * GET /health → { ok: true }\n */\n\nimport http from 'node:http'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { EventEmitter } from 'node:events'\n\nconst port = Number(process.env.PORT ?? 3000)\nconst METRICS_FILE = path.join(process.cwd(), '.tw-cache', 'metrics.json')\nconst MAX_HISTORY = 100\n\n// ─── In-memory metrics store ────────────────────────────────────────────────\nlet currentMetrics = {\n generatedAt: new Date().toISOString(),\n buildMs: null,\n scanMs: null,\n memoryMb: null,\n classCount: null,\n fileCount: null,\n cssBytes: null,\n mode: 'idle',\n}\n\nconst history = []\nconst events = new EventEmitter()\n\n/**\n * Update metrics — dipanggil dari engine atau via file watch\n */\nfunction updateMetrics(data) {\n currentMetrics = { ...currentMetrics, ...data, generatedAt: new Date().toISOString() }\n history.push({ ...currentMetrics })\n if (history.length > MAX_HISTORY) history.shift()\n events.emit('update', currentMetrics)\n}\n\n// ─── Watch metrics file untuk IPC dari engine ───────────────────────────────\nfunction watchMetricsFile() {\n const dir = path.dirname(METRICS_FILE)\n if (!fs.existsSync(dir)) {\n try { fs.mkdirSync(dir, { recursive: true }) } catch {}\n }\n\n if (fs.existsSync(METRICS_FILE)) {\n try {\n const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'))\n updateMetrics(data)\n } catch {}\n }\n\n try {\n fs.watch(METRICS_FILE, { persistent: false }, (eventType) => {\n if (eventType === 'change') {\n try {\n const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'))\n updateMetrics(data)\n } catch {}\n }\n })\n } catch {\n // File doesn't exist yet — poll instead\n setInterval(() => {\n if (fs.existsSync(METRICS_FILE)) {\n try {\n const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'))\n if (data.generatedAt !== currentMetrics.generatedAt) updateMetrics(data)\n } catch {}\n }\n }, 2000)\n }\n}\n\n// ─── HTML Dashboard UI ──────────────────────────────────────────────────────\nconst dashboardHtml = `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n <title>tailwind-styled dashboard</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }\n :root {\n --bg: #0f1117; --surface: #1a1d2e; --border: #2a2d3e;\n --text: #e2e8f0; --muted: #8892a4; --accent: #38bdf8;\n --green: #4ade80; --amber: #fbbf24; --red: #f87171;\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n }\n body { background: var(--bg); color: var(--text); min-height: 100vh; padding: 2rem }\n h1 { font-size: 1.1rem; color: var(--muted); font-weight: 400; letter-spacing: 0.1em;\n text-transform: uppercase; margin-bottom: 2rem; border-bottom: 1px solid var(--border);\n padding-bottom: 1rem }\n h1 span { color: var(--accent) }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem }\n .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 1.25rem; transition: border-color .2s }\n .card:hover { border-color: var(--accent) }\n .card .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .1em;\n color: var(--muted); margin-bottom: .5rem }\n .card .value { font-size: 1.8rem; font-weight: 700; color: var(--text) }\n .card .value.good { color: var(--green) }\n .card .value.warn { color: var(--amber) }\n .card .value.bad { color: var(--red) }\n .card .unit { font-size: .75rem; color: var(--muted); margin-left: .25rem }\n .section-title { font-size: .75rem; text-transform: uppercase; letter-spacing: .1em;\n color: var(--muted); margin: 2rem 0 .75rem }\n .raw { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 1rem; font-size: .8rem; overflow-x: auto; white-space: pre; max-height: 300px;\n overflow-y: auto }\n .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green);\n display: inline-block; margin-right: .5rem; animation: pulse 2s infinite }\n @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }\n .idle .status-dot { background: var(--muted); animation: none }\n .building .status-dot { background: var(--amber) }\n .error .status-dot { background: var(--red); animation: none }\n #status { font-size: .8rem; color: var(--muted); margin-bottom: 2rem }\n #last-update { font-size: .75rem; color: var(--muted); margin-top: 2rem }\n .history-mini { display: flex; align-items: flex-end; gap: 2px; height: 40px; margin-top: .5rem }\n .history-mini .bar { flex: 1; background: var(--accent); opacity: .4; border-radius: 2px 2px 0 0;\n min-width: 4px; transition: opacity .2s }\n .history-mini .bar:hover { opacity: 1 }\n </style>\n</head>\n<body>\n <h1><span class=\"status-dot\" id=\"dot\"></span>tailwind-styled <span>dashboard</span></h1>\n <div id=\"status\">Connecting...</div>\n\n <div class=\"grid\" id=\"cards\">\n <div class=\"card\"><div class=\"label\">Build time</div><div class=\"value\" id=\"buildMs\">—</div></div>\n <div class=\"card\"><div class=\"label\">Scan time</div><div class=\"value\" id=\"scanMs\">—</div></div>\n <div class=\"card\"><div class=\"label\">Classes</div><div class=\"value\" id=\"classCount\">—</div></div>\n <div class=\"card\"><div class=\"label\">Files</div><div class=\"value\" id=\"fileCount\">—</div></div>\n <div class=\"card\"><div class=\"label\">CSS output</div><div class=\"value\" id=\"cssBytes\">—</div></div>\n <div class=\"card\"><div class=\"label\">Memory (heap)</div><div class=\"value\" id=\"memoryMb\">—</div></div>\n </div>\n\n <div class=\"section-title\">Build time history</div>\n <div class=\"card\">\n <div class=\"history-mini\" id=\"history-chart\"></div>\n </div>\n\n <div class=\"section-title\">Raw metrics</div>\n <div class=\"raw\" id=\"raw-json\">Loading...</div>\n <div id=\"last-update\"></div>\n\n <script>\n let prevGenAt = null\n\n function fmt(v, unit, warn, bad) {\n if (v == null) return '—'\n const el = document.createElement('span')\n el.textContent = typeof v === 'number' ? v.toFixed(unit === 'ms' ? 0 : 1) : v\n if (unit) el.insertAdjacentHTML('beforeend', '<span class=\"unit\">' + unit + '</span>')\n if (bad != null && v > bad) el.className = 'bad'\n else if (warn != null && v > warn) el.className = 'warn'\n else if (v != null) el.className = 'good'\n return el.outerHTML\n }\n\n function fmtBytes(b) {\n if (b == null) return '—'\n if (b < 1024) return b + '<span class=\"unit\">B</span>'\n if (b < 1024*1024) return (b/1024).toFixed(1) + '<span class=\"unit\">KB</span>'\n return (b/1024/1024).toFixed(2) + '<span class=\"unit\">MB</span>'\n }\n\n function renderHistory(history) {\n const chart = document.getElementById('history-chart')\n if (!history.length) return\n const vals = history.map(h => h.buildMs ?? 0).filter(v => v > 0)\n if (!vals.length) return\n const max = Math.max(...vals)\n chart.innerHTML = vals.map(v => {\n const h = max > 0 ? Math.max(4, Math.round((v / max) * 40)) : 4\n return '<div class=\"bar\" style=\"height:' + h + 'px\" title=\"' + v + 'ms\"></div>'\n }).join('')\n }\n\n async function fetchAndRender() {\n try {\n const [mRes, hRes] = await Promise.all([fetch('/metrics'), fetch('/history')])\n const m = await mRes.json()\n const h = await hRes.json()\n\n if (m.generatedAt === prevGenAt) return\n prevGenAt = m.generatedAt\n\n document.getElementById('buildMs').innerHTML = fmt(m.buildMs, 'ms', 500, 2000)\n document.getElementById('scanMs').innerHTML = fmt(m.scanMs, 'ms', 200, 1000)\n document.getElementById('classCount').innerHTML = fmt(m.classCount, null, null, null)\n document.getElementById('fileCount').innerHTML = fmt(m.fileCount, null, null, null)\n document.getElementById('cssBytes').innerHTML = fmtBytes(m.cssBytes)\n document.getElementById('memoryMb').innerHTML = fmt(m.memoryMb?.heapUsed, 'MB', 100, 500)\n document.getElementById('raw-json').textContent = JSON.stringify(m, null, 2)\n document.getElementById('last-update').textContent = 'Last update: ' + new Date(m.generatedAt).toLocaleTimeString()\n document.getElementById('status').textContent = 'Mode: ' + (m.mode ?? 'idle')\n\n const dot = document.getElementById('dot')\n dot.parentElement.className = m.mode ?? 'idle'\n\n renderHistory(h)\n } catch (e) {\n document.getElementById('status').textContent = 'Error: ' + e.message\n }\n }\n\n fetchAndRender()\n setInterval(fetchAndRender, 1500)\n </script>\n</body>\n</html>`\n\n// ─── HTTP server ────────────────────────────────────────────────────────────\nconst server = http.createServer((req, res) => {\n const url = new URL(req.url, `http://localhost:${port}`)\n\n if (url.pathname === '/health') {\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ ok: true }))\n return\n }\n\n if (url.pathname === '/metrics') {\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'no-cache')\n res.end(JSON.stringify(currentMetrics, null, 2))\n return\n }\n\n if (url.pathname === '/history') {\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify(history))\n return\n }\n\n if (url.pathname === '/reset' && req.method === 'POST') {\n history.length = 0\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ ok: true, message: 'History cleared' }))\n return\n }\n\n // Default: serve dashboard HTML\n res.setHeader('content-type', 'text/html; charset=utf-8')\n res.end(dashboardHtml)\n})\n\n// ─── Start ──────────────────────────────────────────────────────────────────\nwatchMetricsFile()\n\nserver.listen(port, () => {\n console.log(`[tailwind-styled] Dashboard: http://localhost:${port}`)\n console.log(`[tailwind-styled] Metrics: http://localhost:${port}/metrics`)\n console.log(`[tailwind-styled] Watching: ${METRICS_FILE}`)\n})\n\n// Export untuk dipakai sebagai module\nexport { updateMetrics, currentMetrics, history, events }\n"]}
@@ -0,0 +1,239 @@
1
+ import http from 'http';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { EventEmitter } from 'events';
5
+
6
+ /* tailwind-styled-v4 v5.0.1 | MIT | https://github.com/dictionar32/tailwind-styled-v4 */
7
+
8
+ var port = Number(process.env.PORT ?? 3e3);
9
+ var METRICS_FILE = path.join(process.cwd(), ".tw-cache", "metrics.json");
10
+ var MAX_HISTORY = 100;
11
+ var currentMetrics = {
12
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13
+ buildMs: null,
14
+ scanMs: null,
15
+ memoryMb: null,
16
+ classCount: null,
17
+ fileCount: null,
18
+ cssBytes: null,
19
+ mode: "idle"
20
+ };
21
+ var history = [];
22
+ var events = new EventEmitter();
23
+ function updateMetrics(data) {
24
+ currentMetrics = { ...currentMetrics, ...data, generatedAt: (/* @__PURE__ */ new Date()).toISOString() };
25
+ history.push({ ...currentMetrics });
26
+ if (history.length > MAX_HISTORY) history.shift();
27
+ events.emit("update", currentMetrics);
28
+ }
29
+ function watchMetricsFile() {
30
+ const dir = path.dirname(METRICS_FILE);
31
+ if (!fs.existsSync(dir)) {
32
+ try {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ } catch {
35
+ }
36
+ }
37
+ if (fs.existsSync(METRICS_FILE)) {
38
+ try {
39
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, "utf8"));
40
+ updateMetrics(data);
41
+ } catch {
42
+ }
43
+ }
44
+ try {
45
+ fs.watch(METRICS_FILE, { persistent: false }, (eventType) => {
46
+ if (eventType === "change") {
47
+ try {
48
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, "utf8"));
49
+ updateMetrics(data);
50
+ } catch {
51
+ }
52
+ }
53
+ });
54
+ } catch {
55
+ setInterval(() => {
56
+ if (fs.existsSync(METRICS_FILE)) {
57
+ try {
58
+ const data = JSON.parse(fs.readFileSync(METRICS_FILE, "utf8"));
59
+ if (data.generatedAt !== currentMetrics.generatedAt) updateMetrics(data);
60
+ } catch {
61
+ }
62
+ }
63
+ }, 2e3);
64
+ }
65
+ }
66
+ var dashboardHtml = `<!doctype html>
67
+ <html lang="en">
68
+ <head>
69
+ <meta charset="utf-8"/>
70
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
71
+ <title>tailwind-styled dashboard</title>
72
+ <style>
73
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
74
+ :root {
75
+ --bg: #0f1117; --surface: #1a1d2e; --border: #2a2d3e;
76
+ --text: #e2e8f0; --muted: #8892a4; --accent: #38bdf8;
77
+ --green: #4ade80; --amber: #fbbf24; --red: #f87171;
78
+ font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
79
+ }
80
+ body { background: var(--bg); color: var(--text); min-height: 100vh; padding: 2rem }
81
+ h1 { font-size: 1.1rem; color: var(--muted); font-weight: 400; letter-spacing: 0.1em;
82
+ text-transform: uppercase; margin-bottom: 2rem; border-bottom: 1px solid var(--border);
83
+ padding-bottom: 1rem }
84
+ h1 span { color: var(--accent) }
85
+ .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem }
86
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
87
+ padding: 1.25rem; transition: border-color .2s }
88
+ .card:hover { border-color: var(--accent) }
89
+ .card .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .1em;
90
+ color: var(--muted); margin-bottom: .5rem }
91
+ .card .value { font-size: 1.8rem; font-weight: 700; color: var(--text) }
92
+ .card .value.good { color: var(--green) }
93
+ .card .value.warn { color: var(--amber) }
94
+ .card .value.bad { color: var(--red) }
95
+ .card .unit { font-size: .75rem; color: var(--muted); margin-left: .25rem }
96
+ .section-title { font-size: .75rem; text-transform: uppercase; letter-spacing: .1em;
97
+ color: var(--muted); margin: 2rem 0 .75rem }
98
+ .raw { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
99
+ padding: 1rem; font-size: .8rem; overflow-x: auto; white-space: pre; max-height: 300px;
100
+ overflow-y: auto }
101
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green);
102
+ display: inline-block; margin-right: .5rem; animation: pulse 2s infinite }
103
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
104
+ .idle .status-dot { background: var(--muted); animation: none }
105
+ .building .status-dot { background: var(--amber) }
106
+ .error .status-dot { background: var(--red); animation: none }
107
+ #status { font-size: .8rem; color: var(--muted); margin-bottom: 2rem }
108
+ #last-update { font-size: .75rem; color: var(--muted); margin-top: 2rem }
109
+ .history-mini { display: flex; align-items: flex-end; gap: 2px; height: 40px; margin-top: .5rem }
110
+ .history-mini .bar { flex: 1; background: var(--accent); opacity: .4; border-radius: 2px 2px 0 0;
111
+ min-width: 4px; transition: opacity .2s }
112
+ .history-mini .bar:hover { opacity: 1 }
113
+ </style>
114
+ </head>
115
+ <body>
116
+ <h1><span class="status-dot" id="dot"></span>tailwind-styled <span>dashboard</span></h1>
117
+ <div id="status">Connecting...</div>
118
+
119
+ <div class="grid" id="cards">
120
+ <div class="card"><div class="label">Build time</div><div class="value" id="buildMs">\u2014</div></div>
121
+ <div class="card"><div class="label">Scan time</div><div class="value" id="scanMs">\u2014</div></div>
122
+ <div class="card"><div class="label">Classes</div><div class="value" id="classCount">\u2014</div></div>
123
+ <div class="card"><div class="label">Files</div><div class="value" id="fileCount">\u2014</div></div>
124
+ <div class="card"><div class="label">CSS output</div><div class="value" id="cssBytes">\u2014</div></div>
125
+ <div class="card"><div class="label">Memory (heap)</div><div class="value" id="memoryMb">\u2014</div></div>
126
+ </div>
127
+
128
+ <div class="section-title">Build time history</div>
129
+ <div class="card">
130
+ <div class="history-mini" id="history-chart"></div>
131
+ </div>
132
+
133
+ <div class="section-title">Raw metrics</div>
134
+ <div class="raw" id="raw-json">Loading...</div>
135
+ <div id="last-update"></div>
136
+
137
+ <script>
138
+ let prevGenAt = null
139
+
140
+ function fmt(v, unit, warn, bad) {
141
+ if (v == null) return '\u2014'
142
+ const el = document.createElement('span')
143
+ el.textContent = typeof v === 'number' ? v.toFixed(unit === 'ms' ? 0 : 1) : v
144
+ if (unit) el.insertAdjacentHTML('beforeend', '<span class="unit">' + unit + '</span>')
145
+ if (bad != null && v > bad) el.className = 'bad'
146
+ else if (warn != null && v > warn) el.className = 'warn'
147
+ else if (v != null) el.className = 'good'
148
+ return el.outerHTML
149
+ }
150
+
151
+ function fmtBytes(b) {
152
+ if (b == null) return '\u2014'
153
+ if (b < 1024) return b + '<span class="unit">B</span>'
154
+ if (b < 1024*1024) return (b/1024).toFixed(1) + '<span class="unit">KB</span>'
155
+ return (b/1024/1024).toFixed(2) + '<span class="unit">MB</span>'
156
+ }
157
+
158
+ function renderHistory(history) {
159
+ const chart = document.getElementById('history-chart')
160
+ if (!history.length) return
161
+ const vals = history.map(h => h.buildMs ?? 0).filter(v => v > 0)
162
+ if (!vals.length) return
163
+ const max = Math.max(...vals)
164
+ chart.innerHTML = vals.map(v => {
165
+ const h = max > 0 ? Math.max(4, Math.round((v / max) * 40)) : 4
166
+ return '<div class="bar" style="height:' + h + 'px" title="' + v + 'ms"></div>'
167
+ }).join('')
168
+ }
169
+
170
+ async function fetchAndRender() {
171
+ try {
172
+ const [mRes, hRes] = await Promise.all([fetch('/metrics'), fetch('/history')])
173
+ const m = await mRes.json()
174
+ const h = await hRes.json()
175
+
176
+ if (m.generatedAt === prevGenAt) return
177
+ prevGenAt = m.generatedAt
178
+
179
+ document.getElementById('buildMs').innerHTML = fmt(m.buildMs, 'ms', 500, 2000)
180
+ document.getElementById('scanMs').innerHTML = fmt(m.scanMs, 'ms', 200, 1000)
181
+ document.getElementById('classCount').innerHTML = fmt(m.classCount, null, null, null)
182
+ document.getElementById('fileCount').innerHTML = fmt(m.fileCount, null, null, null)
183
+ document.getElementById('cssBytes').innerHTML = fmtBytes(m.cssBytes)
184
+ document.getElementById('memoryMb').innerHTML = fmt(m.memoryMb?.heapUsed, 'MB', 100, 500)
185
+ document.getElementById('raw-json').textContent = JSON.stringify(m, null, 2)
186
+ document.getElementById('last-update').textContent = 'Last update: ' + new Date(m.generatedAt).toLocaleTimeString()
187
+ document.getElementById('status').textContent = 'Mode: ' + (m.mode ?? 'idle')
188
+
189
+ const dot = document.getElementById('dot')
190
+ dot.parentElement.className = m.mode ?? 'idle'
191
+
192
+ renderHistory(h)
193
+ } catch (e) {
194
+ document.getElementById('status').textContent = 'Error: ' + e.message
195
+ }
196
+ }
197
+
198
+ fetchAndRender()
199
+ setInterval(fetchAndRender, 1500)
200
+ </script>
201
+ </body>
202
+ </html>`;
203
+ var server = http.createServer((req, res) => {
204
+ const url = new URL(req.url, `http://localhost:${port}`);
205
+ if (url.pathname === "/health") {
206
+ res.setHeader("content-type", "application/json");
207
+ res.end(JSON.stringify({ ok: true }));
208
+ return;
209
+ }
210
+ if (url.pathname === "/metrics") {
211
+ res.setHeader("content-type", "application/json");
212
+ res.setHeader("cache-control", "no-cache");
213
+ res.end(JSON.stringify(currentMetrics, null, 2));
214
+ return;
215
+ }
216
+ if (url.pathname === "/history") {
217
+ res.setHeader("content-type", "application/json");
218
+ res.end(JSON.stringify(history));
219
+ return;
220
+ }
221
+ if (url.pathname === "/reset" && req.method === "POST") {
222
+ history.length = 0;
223
+ res.setHeader("content-type", "application/json");
224
+ res.end(JSON.stringify({ ok: true, message: "History cleared" }));
225
+ return;
226
+ }
227
+ res.setHeader("content-type", "text/html; charset=utf-8");
228
+ res.end(dashboardHtml);
229
+ });
230
+ watchMetricsFile();
231
+ server.listen(port, () => {
232
+ console.log(`[tailwind-styled] Dashboard: http://localhost:${port}`);
233
+ console.log(`[tailwind-styled] Metrics: http://localhost:${port}/metrics`);
234
+ console.log(`[tailwind-styled] Watching: ${METRICS_FILE}`);
235
+ });
236
+
237
+ export { currentMetrics, events, history, updateMetrics };
238
+ //# sourceMappingURL=dashboard.mjs.map
239
+ //# sourceMappingURL=dashboard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../packages/dashboard/src/server.mjs"],"names":[],"mappings":"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","file":"dashboard.mjs","sourcesContent":["/**\n * tailwind-styled-v4 Dashboard Server\n *\n * HTTP server yang expose LIVE build metrics dari engine.\n * Bukan lagi hardcoded — connect ke engine via file-based IPC atau\n * event emitter jika dijalankan dalam proses yang sama.\n *\n * Port default: 3000 (override via PORT env var)\n *\n * Endpoints:\n * GET / → HTML dashboard UI\n * GET /metrics → JSON metrics snapshot (live)\n * GET /history → JSON array dari metrics snapshots (max 100)\n * POST /reset → Reset history\n * GET /health → { ok: true }\n */\n\nimport http from 'node:http'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { EventEmitter } from 'node:events'\n\nconst port = Number(process.env.PORT ?? 3000)\nconst METRICS_FILE = path.join(process.cwd(), '.tw-cache', 'metrics.json')\nconst MAX_HISTORY = 100\n\n// ─── In-memory metrics store ────────────────────────────────────────────────\nlet currentMetrics = {\n generatedAt: new Date().toISOString(),\n buildMs: null,\n scanMs: null,\n memoryMb: null,\n classCount: null,\n fileCount: null,\n cssBytes: null,\n mode: 'idle',\n}\n\nconst history = []\nconst events = new EventEmitter()\n\n/**\n * Update metrics — dipanggil dari engine atau via file watch\n */\nfunction updateMetrics(data) {\n currentMetrics = { ...currentMetrics, ...data, generatedAt: new Date().toISOString() }\n history.push({ ...currentMetrics })\n if (history.length > MAX_HISTORY) history.shift()\n events.emit('update', currentMetrics)\n}\n\n// ─── Watch metrics file untuk IPC dari engine ───────────────────────────────\nfunction watchMetricsFile() {\n const dir = path.dirname(METRICS_FILE)\n if (!fs.existsSync(dir)) {\n try { fs.mkdirSync(dir, { recursive: true }) } catch {}\n }\n\n if (fs.existsSync(METRICS_FILE)) {\n try {\n const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'))\n updateMetrics(data)\n } catch {}\n }\n\n try {\n fs.watch(METRICS_FILE, { persistent: false }, (eventType) => {\n if (eventType === 'change') {\n try {\n const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'))\n updateMetrics(data)\n } catch {}\n }\n })\n } catch {\n // File doesn't exist yet — poll instead\n setInterval(() => {\n if (fs.existsSync(METRICS_FILE)) {\n try {\n const data = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'))\n if (data.generatedAt !== currentMetrics.generatedAt) updateMetrics(data)\n } catch {}\n }\n }, 2000)\n }\n}\n\n// ─── HTML Dashboard UI ──────────────────────────────────────────────────────\nconst dashboardHtml = `<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\"/>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n <title>tailwind-styled dashboard</title>\n <style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }\n :root {\n --bg: #0f1117; --surface: #1a1d2e; --border: #2a2d3e;\n --text: #e2e8f0; --muted: #8892a4; --accent: #38bdf8;\n --green: #4ade80; --amber: #fbbf24; --red: #f87171;\n font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;\n }\n body { background: var(--bg); color: var(--text); min-height: 100vh; padding: 2rem }\n h1 { font-size: 1.1rem; color: var(--muted); font-weight: 400; letter-spacing: 0.1em;\n text-transform: uppercase; margin-bottom: 2rem; border-bottom: 1px solid var(--border);\n padding-bottom: 1rem }\n h1 span { color: var(--accent) }\n .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem }\n .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 1.25rem; transition: border-color .2s }\n .card:hover { border-color: var(--accent) }\n .card .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: .1em;\n color: var(--muted); margin-bottom: .5rem }\n .card .value { font-size: 1.8rem; font-weight: 700; color: var(--text) }\n .card .value.good { color: var(--green) }\n .card .value.warn { color: var(--amber) }\n .card .value.bad { color: var(--red) }\n .card .unit { font-size: .75rem; color: var(--muted); margin-left: .25rem }\n .section-title { font-size: .75rem; text-transform: uppercase; letter-spacing: .1em;\n color: var(--muted); margin: 2rem 0 .75rem }\n .raw { background: var(--surface); border: 1px solid var(--border); border-radius: 8px;\n padding: 1rem; font-size: .8rem; overflow-x: auto; white-space: pre; max-height: 300px;\n overflow-y: auto }\n .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green);\n display: inline-block; margin-right: .5rem; animation: pulse 2s infinite }\n @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }\n .idle .status-dot { background: var(--muted); animation: none }\n .building .status-dot { background: var(--amber) }\n .error .status-dot { background: var(--red); animation: none }\n #status { font-size: .8rem; color: var(--muted); margin-bottom: 2rem }\n #last-update { font-size: .75rem; color: var(--muted); margin-top: 2rem }\n .history-mini { display: flex; align-items: flex-end; gap: 2px; height: 40px; margin-top: .5rem }\n .history-mini .bar { flex: 1; background: var(--accent); opacity: .4; border-radius: 2px 2px 0 0;\n min-width: 4px; transition: opacity .2s }\n .history-mini .bar:hover { opacity: 1 }\n </style>\n</head>\n<body>\n <h1><span class=\"status-dot\" id=\"dot\"></span>tailwind-styled <span>dashboard</span></h1>\n <div id=\"status\">Connecting...</div>\n\n <div class=\"grid\" id=\"cards\">\n <div class=\"card\"><div class=\"label\">Build time</div><div class=\"value\" id=\"buildMs\">—</div></div>\n <div class=\"card\"><div class=\"label\">Scan time</div><div class=\"value\" id=\"scanMs\">—</div></div>\n <div class=\"card\"><div class=\"label\">Classes</div><div class=\"value\" id=\"classCount\">—</div></div>\n <div class=\"card\"><div class=\"label\">Files</div><div class=\"value\" id=\"fileCount\">—</div></div>\n <div class=\"card\"><div class=\"label\">CSS output</div><div class=\"value\" id=\"cssBytes\">—</div></div>\n <div class=\"card\"><div class=\"label\">Memory (heap)</div><div class=\"value\" id=\"memoryMb\">—</div></div>\n </div>\n\n <div class=\"section-title\">Build time history</div>\n <div class=\"card\">\n <div class=\"history-mini\" id=\"history-chart\"></div>\n </div>\n\n <div class=\"section-title\">Raw metrics</div>\n <div class=\"raw\" id=\"raw-json\">Loading...</div>\n <div id=\"last-update\"></div>\n\n <script>\n let prevGenAt = null\n\n function fmt(v, unit, warn, bad) {\n if (v == null) return '—'\n const el = document.createElement('span')\n el.textContent = typeof v === 'number' ? v.toFixed(unit === 'ms' ? 0 : 1) : v\n if (unit) el.insertAdjacentHTML('beforeend', '<span class=\"unit\">' + unit + '</span>')\n if (bad != null && v > bad) el.className = 'bad'\n else if (warn != null && v > warn) el.className = 'warn'\n else if (v != null) el.className = 'good'\n return el.outerHTML\n }\n\n function fmtBytes(b) {\n if (b == null) return '—'\n if (b < 1024) return b + '<span class=\"unit\">B</span>'\n if (b < 1024*1024) return (b/1024).toFixed(1) + '<span class=\"unit\">KB</span>'\n return (b/1024/1024).toFixed(2) + '<span class=\"unit\">MB</span>'\n }\n\n function renderHistory(history) {\n const chart = document.getElementById('history-chart')\n if (!history.length) return\n const vals = history.map(h => h.buildMs ?? 0).filter(v => v > 0)\n if (!vals.length) return\n const max = Math.max(...vals)\n chart.innerHTML = vals.map(v => {\n const h = max > 0 ? Math.max(4, Math.round((v / max) * 40)) : 4\n return '<div class=\"bar\" style=\"height:' + h + 'px\" title=\"' + v + 'ms\"></div>'\n }).join('')\n }\n\n async function fetchAndRender() {\n try {\n const [mRes, hRes] = await Promise.all([fetch('/metrics'), fetch('/history')])\n const m = await mRes.json()\n const h = await hRes.json()\n\n if (m.generatedAt === prevGenAt) return\n prevGenAt = m.generatedAt\n\n document.getElementById('buildMs').innerHTML = fmt(m.buildMs, 'ms', 500, 2000)\n document.getElementById('scanMs').innerHTML = fmt(m.scanMs, 'ms', 200, 1000)\n document.getElementById('classCount').innerHTML = fmt(m.classCount, null, null, null)\n document.getElementById('fileCount').innerHTML = fmt(m.fileCount, null, null, null)\n document.getElementById('cssBytes').innerHTML = fmtBytes(m.cssBytes)\n document.getElementById('memoryMb').innerHTML = fmt(m.memoryMb?.heapUsed, 'MB', 100, 500)\n document.getElementById('raw-json').textContent = JSON.stringify(m, null, 2)\n document.getElementById('last-update').textContent = 'Last update: ' + new Date(m.generatedAt).toLocaleTimeString()\n document.getElementById('status').textContent = 'Mode: ' + (m.mode ?? 'idle')\n\n const dot = document.getElementById('dot')\n dot.parentElement.className = m.mode ?? 'idle'\n\n renderHistory(h)\n } catch (e) {\n document.getElementById('status').textContent = 'Error: ' + e.message\n }\n }\n\n fetchAndRender()\n setInterval(fetchAndRender, 1500)\n </script>\n</body>\n</html>`\n\n// ─── HTTP server ────────────────────────────────────────────────────────────\nconst server = http.createServer((req, res) => {\n const url = new URL(req.url, `http://localhost:${port}`)\n\n if (url.pathname === '/health') {\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ ok: true }))\n return\n }\n\n if (url.pathname === '/metrics') {\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'no-cache')\n res.end(JSON.stringify(currentMetrics, null, 2))\n return\n }\n\n if (url.pathname === '/history') {\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify(history))\n return\n }\n\n if (url.pathname === '/reset' && req.method === 'POST') {\n history.length = 0\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ ok: true, message: 'History cleared' }))\n return\n }\n\n // Default: serve dashboard HTML\n res.setHeader('content-type', 'text/html; charset=utf-8')\n res.end(dashboardHtml)\n})\n\n// ─── Start ──────────────────────────────────────────────────────────────────\nwatchMetricsFile()\n\nserver.listen(port, () => {\n console.log(`[tailwind-styled] Dashboard: http://localhost:${port}`)\n console.log(`[tailwind-styled] Metrics: http://localhost:${port}/metrics`)\n console.log(`[tailwind-styled] Watching: ${METRICS_FILE}`)\n})\n\n// Export untuk dipakai sebagai module\nexport { updateMetrics, currentMetrics, history, events }\n"]}