@nitronjs/framework 0.2.27 → 0.3.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.
Files changed (58) hide show
  1. package/README.md +260 -170
  2. package/lib/Auth/Auth.js +2 -2
  3. package/lib/Build/CssBuilder.js +5 -7
  4. package/lib/Build/EffectivePropUsage.js +174 -0
  5. package/lib/Build/FactoryTransform.js +1 -21
  6. package/lib/Build/FileAnalyzer.js +1 -32
  7. package/lib/Build/Manager.js +354 -58
  8. package/lib/Build/PropUsageAnalyzer.js +1189 -0
  9. package/lib/Build/jsxRuntime.js +25 -155
  10. package/lib/Build/plugins.js +212 -146
  11. package/lib/Build/propUtils.js +70 -0
  12. package/lib/Console/Commands/DevCommand.js +30 -10
  13. package/lib/Console/Commands/MakeCommand.js +8 -1
  14. package/lib/Console/Output.js +0 -2
  15. package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
  16. package/lib/Console/Stubs/vendor-dev.tsx +30 -41
  17. package/lib/Console/Stubs/vendor.tsx +25 -1
  18. package/lib/Core/Config.js +0 -6
  19. package/lib/Core/Paths.js +0 -19
  20. package/lib/Database/Migration/Checksum.js +0 -3
  21. package/lib/Database/Migration/MigrationRepository.js +0 -8
  22. package/lib/Database/Migration/MigrationRunner.js +1 -2
  23. package/lib/Database/Model.js +19 -11
  24. package/lib/Database/QueryBuilder.js +25 -4
  25. package/lib/Database/Schema/Blueprint.js +10 -0
  26. package/lib/Database/Schema/Manager.js +2 -0
  27. package/lib/Date/DateTime.js +1 -1
  28. package/lib/Dev/DevContext.js +44 -0
  29. package/lib/Dev/DevErrorPage.js +990 -0
  30. package/lib/Dev/DevIndicator.js +836 -0
  31. package/lib/HMR/Server.js +16 -37
  32. package/lib/Http/Server.js +171 -23
  33. package/lib/Logging/Log.js +34 -2
  34. package/lib/Mail/Mail.js +41 -10
  35. package/lib/Route/Router.js +43 -19
  36. package/lib/Runtime/Entry.js +10 -6
  37. package/lib/Session/Manager.js +103 -1
  38. package/lib/Session/Session.js +0 -4
  39. package/lib/Support/Str.js +6 -4
  40. package/lib/Translation/Lang.js +376 -32
  41. package/lib/Translation/pluralize.js +81 -0
  42. package/lib/Validation/MagicBytes.js +120 -0
  43. package/lib/Validation/Validator.js +46 -29
  44. package/lib/View/Client/hmr-client.js +100 -90
  45. package/lib/View/Client/spa.js +121 -50
  46. package/lib/View/ClientManifest.js +60 -0
  47. package/lib/View/FlightRenderer.js +100 -0
  48. package/lib/View/Layout.js +0 -3
  49. package/lib/View/PropFilter.js +81 -0
  50. package/lib/View/View.js +230 -495
  51. package/lib/index.d.ts +22 -1
  52. package/package.json +2 -2
  53. package/skeleton/config/app.js +1 -0
  54. package/skeleton/config/server.js +13 -0
  55. package/skeleton/config/session.js +3 -0
  56. package/lib/Build/HydrationBuilder.js +0 -190
  57. package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
  58. package/lib/Console/Stubs/page-hydration.tsx +0 -53
@@ -0,0 +1,836 @@
1
+ /**
2
+ * DevIndicator — Enhanced development mode indicator panel.
3
+ *
4
+ * Shows NitronJS branding, HMR status, server info, and expandable
5
+ * debug panels for Route, Timing, Middleware, and Props.
6
+ *
7
+ * Only imported via dynamic import() when Environment.isDev is true.
8
+ * Production never loads this module.
9
+ */
10
+
11
+ import { readFileSync } from "fs";
12
+ import { fileURLToPath } from "url";
13
+ import path from "path";
14
+ import Config from "../Core/Config.js";
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const packageJsonPath = path.join(__dirname, "../../package.json");
18
+ const FRAMEWORK_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf-8")).version;
19
+ const ICON_BASE64 = "data:image/png;base64," + readFileSync(path.join(__dirname, "../View/Client/nitronjs-icon.png")).toString("base64");
20
+
21
+ const ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
22
+
23
+ class DevIndicator {
24
+
25
+ static render(nonceAttr, devData = null) {
26
+ const nodeVersion = process.version;
27
+ const port = String(Config.get("server.port", 3000)).replace(/[<>"'&]/g, "");
28
+
29
+ const route = devData?.route;
30
+ const timing = devData ? buildTimingSummary(devData) : null;
31
+ const mwCount = devData?.middlewareTiming?.length || 0;
32
+ const propsInfo = devData?.rawProps ? buildPropsSummary(devData) : null;
33
+
34
+ const totalMs = timing ? parseFloat(timing.total) : 0;
35
+ const timingWarnClass = totalMs >= 300 ? " ndi-critical" : totalMs >= 100 ? " ndi-warn" : "";
36
+
37
+ const propsTreeData = propsInfo ? buildPropsTreeData(devData) : null;
38
+ const propsJson = propsTreeData ? safeJsonEmbed(propsTreeData) : "null";
39
+
40
+ return `
41
+ <div id="__nitron_dev__" style="position:fixed;bottom:16px;left:16px;z-index:999999;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
42
+ <style${nonceAttr}>
43
+ #__nitron_dev__ {
44
+ --bg:#0a0a0a;--surface:rgba(8,8,8,0.96);
45
+ --border:rgba(255,255,255,0.07);--border2:rgba(255,255,255,0.04);
46
+ --text:#e0e0e0;--text2:#aaa;--text3:#888;--text4:#666;--text5:#555;--text6:#444;
47
+ --accent:#3b82f6;--red:#ef4444;--yellow:#fbbf24;--green:#4ade80;
48
+ --blue:#3b82f6;--orange:#f97316;--purple:#a78bfa;
49
+ --mono:'SF Mono','Cascadia Code',Consolas,monospace;
50
+ }
51
+ #__nitron_dev__ * { box-sizing:border-box; }
52
+
53
+ /* ── Button ── */
54
+ #__nitron_dev_btn__ {
55
+ position:relative;display:flex;align-items:center;justify-content:center;
56
+ width:42px;height:42px;padding:0;
57
+ background:var(--bg);color:#fff;border:1px solid rgba(255,255,255,0.1);border-radius:50%;
58
+ cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,0.5);
59
+ transition:all 0.2s ease;touch-action:none;
60
+ }
61
+ #__nitron_dev_btn__:hover {
62
+ transform:scale(1.05);
63
+ box-shadow:0 0 20px rgba(59,130,246,0.15);
64
+ border-color:rgba(59,130,246,0.3);
65
+ }
66
+ #__nitron_dev_btn__.dragging {
67
+ cursor:grabbing;
68
+ box-shadow:0 0 0 2px rgba(59,130,246,0.3),0 12px 40px rgba(0,0,0,0.7);
69
+ transform:scale(1.1);opacity:0.9;
70
+ }
71
+ .ndi-dot {
72
+ position:absolute;top:0;right:2px;
73
+ width:10px;height:10px;border-radius:50%;
74
+ border:2px solid var(--bg);background:var(--green);
75
+ animation:__nitron_pulse 2s infinite;
76
+ }
77
+
78
+ /* ── Panel ── */
79
+ #__nitron_dev_panel__ {
80
+ display:none;position:absolute;width:400px;
81
+ background:var(--surface);
82
+ backdrop-filter:blur(24px);-webkit-backdrop-filter:blur(24px);
83
+ border:1px solid var(--border);border-radius:20px;
84
+ overflow:hidden;
85
+ box-shadow:
86
+ 0 24px 64px rgba(0,0,0,0.6),
87
+ 0 0 0 1px rgba(59,130,246,0.05),
88
+ inset 0 1px 0 rgba(255,255,255,0.04),
89
+ -40px -20px 80px rgba(59,130,246,0.05),
90
+ 40px 30px 60px rgba(167,139,250,0.04);
91
+ }
92
+ #__nitron_dev_panel__::before {
93
+ content:'';display:block;height:2px;
94
+ background:linear-gradient(90deg,var(--accent),var(--purple),var(--accent));
95
+ background-size:200% 100%;
96
+ animation:__nitron_gradient 3s ease infinite;
97
+ }
98
+
99
+ /* ── Header ── */
100
+ .ndi-hdr {
101
+ padding:16px 20px 14px;
102
+ display:flex;align-items:center;gap:12px;
103
+ }
104
+ .ndi-hdr-ring {
105
+ width:36px;height:36px;border-radius:50%;
106
+ border:1.5px solid rgba(59,130,246,0.25);
107
+ display:flex;align-items:center;justify-content:center;
108
+ background:rgba(59,130,246,0.05);flex-shrink:0;
109
+ }
110
+ .ndi-hdr-ring img { width:20px;height:20px;object-fit:contain; }
111
+ .ndi-hdr-text { flex:1;min-width:0; }
112
+ .ndi-hdr-title { font-size:14px;font-weight:600;color:var(--text); }
113
+ .ndi-hdr-ver { font-weight:400;color:var(--text6);font-size:11px; }
114
+ .ndi-hdr-sub { font-size:10px;color:var(--text6);margin-top:1px; }
115
+ .ndi-live {
116
+ display:flex;align-items:center;gap:5px;
117
+ font-size:10px;font-weight:600;color:var(--green);
118
+ background:rgba(74,222,128,0.06);padding:4px 10px;
119
+ border-radius:20px;border:1px solid rgba(74,222,128,0.12);flex-shrink:0;
120
+ }
121
+ .ndi-live-dot {
122
+ width:5px;height:5px;border-radius:50%;background:var(--green);
123
+ animation:__nitron_pulse 2s infinite;
124
+ }
125
+
126
+ /* ── Route Bar ── */
127
+ .ndi-route {
128
+ margin:0 14px;padding:10px 16px;
129
+ background:rgba(255,255,255,0.025);
130
+ border:1px solid var(--border2);border-radius:12px;
131
+ display:flex;align-items:center;gap:10px;
132
+ }
133
+ .ndi-route-label { font-size:10px;text-transform:uppercase;letter-spacing:0.5px;color:var(--text5);flex-shrink:0; }
134
+ .ndi-route-path {
135
+ flex:1;min-width:0;font-size:13px;font-family:var(--mono);
136
+ color:#ccc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
137
+ }
138
+ .ndi-method {
139
+ font-size:10px;font-weight:700;color:var(--green);
140
+ background:rgba(74,222,128,0.08);padding:3px 8px;
141
+ border-radius:5px;flex-shrink:0;
142
+ }
143
+
144
+ /* ── Sections ── */
145
+ .ndi-sections { padding:8px 14px 10px;display:flex;flex-direction:column;gap:2px; }
146
+ .ndi-sec {
147
+ background:rgba(255,255,255,0.02);
148
+ border:1px solid var(--border2);border-radius:12px;
149
+ overflow:hidden;cursor:pointer;
150
+ transition:background 0.2s,border-color 0.2s;
151
+ }
152
+ .ndi-sec:hover { background:rgba(255,255,255,0.035);border-color:rgba(255,255,255,0.06); }
153
+ .ndi-sec.active { border-color:rgba(59,130,246,0.10);background:rgba(59,130,246,0.02); }
154
+ .ndi-sec-head { padding:12px 16px;display:flex;align-items:center;gap:12px; }
155
+ .ndi-sec-icon {
156
+ width:32px;height:32px;border-radius:9px;
157
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
158
+ }
159
+ .ndi-sec-icon.blue { background:rgba(59,130,246,0.08);color:var(--blue); }
160
+ .ndi-sec-icon.orange { background:rgba(249,115,22,0.08);color:var(--orange); }
161
+ .ndi-sec-icon.purple { background:rgba(167,139,250,0.08);color:var(--purple); }
162
+ .ndi-sec-body { flex:1;min-width:0; }
163
+ .ndi-sec-label { font-size:11px;font-weight:600;color:var(--text3);text-transform:uppercase;letter-spacing:0.3px; }
164
+ .ndi-sec-value { font-size:18px;font-weight:300;color:var(--text);line-height:1.3;margin-top:1px; }
165
+ .ndi-sec-value .u { font-size:12px;color:var(--text5);font-weight:400; }
166
+ .ndi-sec-hint { font-size:10px;color:var(--text5);margin-top:1px; }
167
+ .ndi-sec-hint .a { color:var(--red);font-weight:500; }
168
+ .ndi-sec-arrow {
169
+ font-size:10px;color:#333;
170
+ transition:transform 0.2s,color 0.2s;flex-shrink:0;margin-left:auto;
171
+ }
172
+ .ndi-sec:hover .ndi-sec-arrow { color:var(--accent); }
173
+ .ndi-sec.active .ndi-sec-arrow { transform:rotate(90deg);color:var(--accent); }
174
+ .ndi-warn .ndi-sec-value { color:var(--yellow); }
175
+ .ndi-critical .ndi-sec-value { color:var(--red); }
176
+
177
+ /* ── Section Details ── */
178
+ .ndi-sec-details { max-height:0;overflow:hidden;transition:max-height 0.3s ease-out; }
179
+ .ndi-sec-details-inner { padding:0 16px 14px;margin-top:-2px; }
180
+ .ndi-sec-divider { height:1px;background:var(--border2);margin-bottom:10px; }
181
+ .ndi-d-row { display:flex;justify-content:space-between;align-items:center;padding:5px 0;font-size:12px; }
182
+ .ndi-d-key { color:var(--text4);display:flex;align-items:center;gap:6px; }
183
+ .ndi-d-val { color:var(--text2);font-family:var(--mono);font-size:12px; }
184
+ .ndi-d-dot { width:6px;height:6px;border-radius:50%;flex-shrink:0; }
185
+ .ndi-timing-bar {
186
+ display:flex;height:4px;border-radius:3px;overflow:hidden;
187
+ background:rgba(255,255,255,0.04);margin-bottom:10px;
188
+ }
189
+
190
+ /* Middleware list */
191
+ .ndi-mw-item { display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px; }
192
+ .ndi-mw-dot { width:4px;height:4px;border-radius:50%;background:var(--orange);flex-shrink:0;opacity:0.6; }
193
+ .ndi-mw-name { color:var(--text2);font-family:var(--mono); }
194
+ .ndi-mw-time { margin-left:auto;color:var(--text5);font-family:var(--mono);font-size:11px; }
195
+
196
+ /* ── Props Scroll ── */
197
+ .ndi-props-scroll {
198
+ max-height:300px;overflow-y:auto;overflow-x:hidden;
199
+ overscroll-behavior:contain;
200
+ margin:0 -4px;padding:0 4px;
201
+ scrollbar-width:thin;
202
+ scrollbar-color:rgba(255,255,255,0.08) transparent;
203
+ }
204
+ .ndi-props-scroll::-webkit-scrollbar { width:4px; }
205
+ .ndi-props-scroll::-webkit-scrollbar-track { background:transparent; }
206
+ .ndi-props-scroll::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.08);border-radius:4px; }
207
+ .ndi-props-scroll::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,0.14); }
208
+
209
+ /* ── Tree View ── */
210
+ .ndi-tree { font-family:var(--mono);font-size:12px; }
211
+ .ndi-tree-row {
212
+ display:flex;align-items:flex-start;padding:4px 0;
213
+ min-height:24px;cursor:default;border-radius:4px;transition:background 0.1s;
214
+ }
215
+ .ndi-tree-row:hover { background:rgba(255,255,255,0.02); }
216
+ .ndi-tree-row.exp { cursor:pointer; }
217
+ .ndi-tree-guide { display:inline-block;width:20px;position:relative;flex-shrink:0;align-self:stretch; }
218
+ .ndi-tree-guide::before {
219
+ content:'';position:absolute;left:8px;top:0;bottom:0;
220
+ width:1px;background:rgba(255,255,255,0.06);
221
+ }
222
+ .ndi-tree-arrow {
223
+ display:inline-block;width:16px;flex-shrink:0;color:var(--text6);font-size:9px;
224
+ line-height:24px;text-align:center;transition:transform 0.15s,color 0.15s;user-select:none;
225
+ }
226
+ .ndi-tree-row.exp:hover .ndi-tree-arrow { color:var(--accent); }
227
+ .ndi-tree-row.exp.open .ndi-tree-arrow { transform:rotate(90deg);color:var(--accent); }
228
+ .ndi-tree-arrow.h { visibility:hidden; }
229
+ .ndi-tree-key { color:#c4a0ff;margin-right:4px;white-space:nowrap; }
230
+ .ndi-tree-colon { color:var(--text6);margin-right:6px; }
231
+ .ndi-tree-str { color:#7dd3a8; }
232
+ .ndi-tree-str::before,.ndi-tree-str::after { content:'"';color:#5a9e7d; }
233
+ .ndi-tree-num { color:#79b8ff; }
234
+ .ndi-tree-bool { color:#f97583; }
235
+ .ndi-tree-null { color:var(--text4);font-style:italic; }
236
+ .ndi-tree-badge {
237
+ font-size:10px;color:var(--text4);
238
+ background:rgba(255,255,255,0.03);padding:1px 6px;
239
+ border-radius:3px;border:1px solid var(--border2);white-space:nowrap;
240
+ }
241
+ .ndi-tree-children { /* controlled by JS display toggle */ }
242
+
243
+ /* Filtered props separator */
244
+ .ndi-tree-sep { display:flex;align-items:center;gap:8px;padding:10px 0 6px; }
245
+ .ndi-tree-sep-line { flex:1;height:1px;background:var(--border2); }
246
+ .ndi-tree-sep-text {
247
+ font-size:9px;font-weight:600;text-transform:uppercase;
248
+ letter-spacing:0.8px;color:var(--red);flex-shrink:0;
249
+ }
250
+ .ndi-tree-row.filtered .ndi-tree-key {
251
+ color:var(--text3);text-decoration:line-through;
252
+ text-decoration-color:rgba(239,68,68,0.5);
253
+ }
254
+ .ndi-tree-stripped {
255
+ font-size:9px;color:var(--red);
256
+ background:rgba(239,68,68,0.08);padding:1px 6px;
257
+ border-radius:3px;border:1px solid rgba(239,68,68,0.12);margin-left:6px;
258
+ }
259
+
260
+ /* "N more items" button */
261
+ .ndi-tree-more {
262
+ display:flex;align-items:center;gap:6px;padding:5px 0 3px;
263
+ font-size:11px;color:var(--accent);cursor:pointer;user-select:none;
264
+ transition:color 0.15s;
265
+ }
266
+ .ndi-tree-more:hover { color:#60a5fa; }
267
+ .ndi-tree-more-dots { letter-spacing:2px;opacity:0.5; }
268
+
269
+ /* ── Footer ── */
270
+ .ndi-footer {
271
+ display:flex;align-items:center;gap:16px;
272
+ padding:10px 20px 12px;border-top:1px solid var(--border2);
273
+ }
274
+ .ndi-foot { display:flex;align-items:center;gap:5px;font-size:11px;color:var(--text6); }
275
+ .ndi-foot-val { color:var(--text4); }
276
+ .ndi-foot-dot { width:5px;height:5px;border-radius:50%; }
277
+ .ndi-foot-dot.ok { background:var(--green); }
278
+ .ndi-foot-dot.err { background:var(--red); }
279
+ .ndi-foot-dot.warn { background:var(--yellow); }
280
+
281
+ /* ── Tooltip ── */
282
+ .ndi-tooltip {
283
+ position:absolute;white-space:nowrap;padding:5px 10px;
284
+ background:rgba(20,20,20,0.95);border:1px solid rgba(59,130,246,0.3);border-radius:6px;
285
+ font-size:11px;color:var(--text2);pointer-events:none;transition:opacity 0.3s;
286
+ }
287
+
288
+ /* ── Keyframes ── */
289
+ @keyframes __nitron_slideUp { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
290
+ @keyframes __nitron_slideDown { from{opacity:0;transform:translateY(-8px)} to{opacity:1;transform:translateY(0)} }
291
+ @keyframes __nitron_pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
292
+ @keyframes __nitron_gradient { 0%,100%{background-position:0% 50%} 50%{background-position:100% 50%} }
293
+ </style>
294
+
295
+ <button id="__nitron_dev_btn__" title="NitronJS Dev Tools \u2014 Drag to reposition">
296
+ <img src="${ICON_BASE64}" alt="N" style="width:20px;height:20px;">
297
+ <span class="ndi-dot" id="__nitron_status_dot__"></span>
298
+ </button>
299
+
300
+ <div id="__nitron_dev_panel__">
301
+ <div class="ndi-hdr">
302
+ <div class="ndi-hdr-ring"><img src="${ICON_BASE64}" alt="N"></div>
303
+ <div class="ndi-hdr-text">
304
+ <div class="ndi-hdr-title">NitronJS <span class="ndi-hdr-ver">v${esc(FRAMEWORK_VERSION)}</span></div>
305
+ <div class="ndi-hdr-sub">localhost:${port}</div>
306
+ </div>
307
+ <div class="ndi-live" id="__nitron_hmr_badge__">
308
+ <span class="ndi-live-dot" id="__nitron_hmr_dot__"></span>
309
+ <span id="__nitron_hmr_txt__">HMR</span>
310
+ </div>
311
+ </div>
312
+ ${route ? `
313
+ <div class="ndi-route">
314
+ <span class="ndi-route-label">Route</span>
315
+ <span class="ndi-route-path">${esc(route.pattern)}</span>
316
+ <span class="ndi-method">${esc(route.method)}</span>
317
+ </div>` : ""}
318
+
319
+ <div class="ndi-sections">
320
+ ${timing ? `
321
+ <div class="ndi-sec${timingWarnClass}" data-section="response">
322
+ <div class="ndi-sec-head">
323
+ <div class="ndi-sec-icon blue">
324
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
325
+ </div>
326
+ <div class="ndi-sec-body">
327
+ <div class="ndi-sec-label">Response</div>
328
+ <div class="ndi-sec-value">${timing.total} <span class="u">ms</span></div>
329
+ </div>
330
+ <span class="ndi-sec-arrow">\u25B8</span>
331
+ </div>
332
+ <div class="ndi-sec-details" id="ndi-det-response">
333
+ <div class="ndi-sec-details-inner">
334
+ <div class="ndi-sec-divider"></div>
335
+ <div class="ndi-timing-bar">
336
+ ${timing.mwPct > 0 ? `<div style="width:${timing.mwPct}%;background:var(--blue);border-radius:3px 0 0 3px"></div>` : ""}
337
+ ${timing.ctrlPct > 0 ? `<div style="width:${timing.ctrlPct}%;background:var(--orange)"></div>` : ""}
338
+ ${timing.renderPct > 0 ? `<div style="width:${timing.renderPct}%;background:var(--purple);border-radius:0 3px 3px 0"></div>` : ""}
339
+ </div>
340
+ ${timing.middlewareTotal > 0 ? `<div class="ndi-d-row"><span class="ndi-d-key"><span class="ndi-d-dot" style="background:var(--blue)"></span> Middleware</span><span class="ndi-d-val">${timing.middlewareTotal}ms</span></div>` : ""}
341
+ ${timing.controller > 0 ? `<div class="ndi-d-row"><span class="ndi-d-key"><span class="ndi-d-dot" style="background:var(--orange)"></span> Controller</span><span class="ndi-d-val">${timing.controller}ms</span></div>` : ""}
342
+ ${timing.render > 0 ? `<div class="ndi-d-row"><span class="ndi-d-key"><span class="ndi-d-dot" style="background:var(--purple)"></span> Render</span><span class="ndi-d-val">${timing.render}ms</span></div>` : ""}
343
+ </div>
344
+ </div>
345
+ </div>` : ""}
346
+ ${mwCount > 0 ? `
347
+ <div class="ndi-sec" data-section="middleware">
348
+ <div class="ndi-sec-head">
349
+ <div class="ndi-sec-icon orange">
350
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
351
+ </div>
352
+ <div class="ndi-sec-body">
353
+ <div class="ndi-sec-label">Middleware</div>
354
+ <div class="ndi-sec-value">${mwCount} <span class="u">applied</span></div>
355
+ </div>
356
+ <span class="ndi-sec-arrow">\u25B8</span>
357
+ </div>
358
+ <div class="ndi-sec-details" id="ndi-det-middleware">
359
+ <div class="ndi-sec-details-inner">
360
+ <div class="ndi-sec-divider"></div>
361
+ ${(devData.middlewareTiming || []).map(function(m) {
362
+ return '<div class="ndi-mw-item"><span class="ndi-mw-dot"></span><span class="ndi-mw-name">' + esc(m.name) + '</span><span class="ndi-mw-time">' + (m.duration ?? 0).toFixed(1) + 'ms</span></div>';
363
+ }).join("")}
364
+ </div>
365
+ </div>
366
+ </div>` : ""}
367
+ ${propsInfo ? `
368
+ <div class="ndi-sec" data-section="props">
369
+ <div class="ndi-sec-head">
370
+ <div class="ndi-sec-icon purple">
371
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/></svg>
372
+ </div>
373
+ <div class="ndi-sec-body">
374
+ <div class="ndi-sec-label">Props</div>
375
+ <div class="ndi-sec-value">${esc(String(propsInfo.sent))} <span class="u">sent</span></div>
376
+ ${propsInfo.removed > 0 ? '<div class="ndi-sec-hint"><span class="a">' + esc(String(propsInfo.removed)) + ' filtered</span></div>' : ""}
377
+ </div>
378
+ <span class="ndi-sec-arrow">\u25B8</span>
379
+ </div>
380
+ <div class="ndi-sec-details" id="ndi-det-props">
381
+ <div class="ndi-sec-details-inner">
382
+ <div class="ndi-sec-divider"></div>
383
+ <div class="ndi-props-scroll">
384
+ <div class="ndi-tree" id="ndi-props-tree"></div>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ </div>` : ""}
389
+ </div>
390
+
391
+ <div class="ndi-footer">
392
+ <div class="ndi-foot"><span class="ndi-foot-dot ok" id="__nitron_foot_dot__"></span> <span class="ndi-foot-val" id="__nitron_foot_hmr__">HMR</span></div>
393
+ <div class="ndi-foot">Node <span class="ndi-foot-val">${esc(nodeVersion)}</span></div>
394
+ ${route?.name ? ' <div class="ndi-foot" style="margin-left:auto"><span class="ndi-foot-val">' + esc(route.name) + "</span></div>" : ""}
395
+ </div>
396
+ </div>
397
+ </div>
398
+ <script${nonceAttr}>
399
+ (function(){
400
+ var PROPS_DATA = ${propsJson};
401
+ var ARRAY_INITIAL = 3;
402
+ var DRAG_THRESHOLD = 5;
403
+ var EDGE_MARGIN = 16;
404
+ var startX, startY, isDragging = false;
405
+ var currentCorner = 'bottom-left';
406
+ var panelOpen = false;
407
+ var closingTimer = null;
408
+ var propsTreeBuilt = false;
409
+ var moreCounter = 0;
410
+
411
+ var container = document.getElementById('__nitron_dev__');
412
+ var btn = document.getElementById('__nitron_dev_btn__');
413
+ var panel = document.getElementById('__nitron_dev_panel__');
414
+ var dot = document.getElementById('__nitron_status_dot__');
415
+ var hmrDot = document.getElementById('__nitron_hmr_dot__');
416
+ var hmrTxt = document.getElementById('__nitron_hmr_txt__');
417
+ var hmrBadge = document.getElementById('__nitron_hmr_badge__');
418
+ var footDot = document.getElementById('__nitron_foot_dot__');
419
+ var footHmr = document.getElementById('__nitron_foot_hmr__');
420
+
421
+ var hasStoredPos = false;
422
+
423
+ try {
424
+ var saved = JSON.parse(localStorage.getItem('__nitron_indicator_pos__'));
425
+ if (saved && saved.corner) { currentCorner = saved.corner; hasStoredPos = true; }
426
+ }
427
+ catch (e) {}
428
+
429
+ applyCorner(currentCorner, false);
430
+
431
+ if (!hasStoredPos) {
432
+ var tip = document.createElement('div');
433
+ tip.className = 'ndi-tooltip';
434
+ tip.textContent = 'Grab to reposition';
435
+ positionTooltip(tip);
436
+ container.appendChild(tip);
437
+ setTimeout(function() {
438
+ tip.style.opacity = '0';
439
+ setTimeout(function() { tip.remove(); }, 300);
440
+ }, 3000);
441
+ }
442
+
443
+ /* ── Drag System ── */
444
+ btn.addEventListener('pointerdown', function(e) {
445
+ e.preventDefault();
446
+ startX = e.clientX;
447
+ startY = e.clientY;
448
+ isDragging = false;
449
+ btn.setPointerCapture(e.pointerId);
450
+ });
451
+
452
+ btn.addEventListener('pointermove', function(e) {
453
+ if (startX === undefined) return;
454
+ var dx = e.clientX - startX, dy = e.clientY - startY;
455
+
456
+ if (!isDragging && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
457
+ isDragging = true;
458
+ btn.classList.add('dragging');
459
+ closePanel();
460
+ }
461
+
462
+ if (isDragging) {
463
+ container.style.transition = 'none';
464
+ container.style.left = (e.clientX - 21) + 'px';
465
+ container.style.top = (e.clientY - 21) + 'px';
466
+ container.style.right = 'auto';
467
+ container.style.bottom = 'auto';
468
+ }
469
+ });
470
+
471
+ btn.addEventListener('pointerup', function(e) {
472
+ if (isDragging) {
473
+ btn.classList.remove('dragging');
474
+ currentCorner = getClosestCorner(e.clientX, e.clientY);
475
+ applyCorner(currentCorner, true);
476
+ try { localStorage.setItem('__nitron_indicator_pos__', JSON.stringify({ corner: currentCorner })); }
477
+ catch (e) {}
478
+ }
479
+ else if (startX !== undefined) {
480
+ togglePanel();
481
+ }
482
+ startX = startY = undefined;
483
+ });
484
+
485
+ btn.addEventListener('pointercancel', function() {
486
+ btn.classList.remove('dragging');
487
+ isDragging = false;
488
+ startX = startY = undefined;
489
+ });
490
+
491
+ document.addEventListener('click', function(e) {
492
+ if (e.target.closest('#__nitron_dev_btn__')) return;
493
+ if (!e.target.closest('#__nitron_dev__')) closePanel();
494
+ });
495
+
496
+ /* ── Corner Positioning ── */
497
+ function getClosestCorner(x, y) {
498
+ var vw = window.innerWidth, vh = window.innerHeight;
499
+ return (y > vh / 2 ? 'bottom' : 'top') + '-' + (x > vw / 2 ? 'right' : 'left');
500
+ }
501
+
502
+ function applyCorner(corner, animate) {
503
+ var s = container.style;
504
+ if (animate) {
505
+ s.transition = 'left 0.3s cubic-bezier(0.25,1,0.5,1),right 0.3s cubic-bezier(0.25,1,0.5,1),top 0.3s cubic-bezier(0.25,1,0.5,1),bottom 0.3s cubic-bezier(0.25,1,0.5,1)';
506
+ setTimeout(function() { s.transition = ''; }, 350);
507
+ }
508
+ else {
509
+ s.transition = '';
510
+ }
511
+ s.left = corner.includes('left') ? EDGE_MARGIN + 'px' : 'auto';
512
+ s.right = corner.includes('right') ? EDGE_MARGIN + 'px' : 'auto';
513
+ s.top = corner.includes('top') ? EDGE_MARGIN + 'px' : 'auto';
514
+ s.bottom = corner.includes('bottom') ? EDGE_MARGIN + 'px' : 'auto';
515
+ }
516
+
517
+ function positionTooltip(tip) {
518
+ var isRight = currentCorner.includes('right');
519
+ var isTop = currentCorner.includes('top');
520
+ if (isRight) { tip.style.right = '52px'; tip.style.left = 'auto'; }
521
+ else { tip.style.left = '52px'; tip.style.right = 'auto'; }
522
+ if (isTop) { tip.style.top = '10px'; tip.style.bottom = 'auto'; }
523
+ else { tip.style.bottom = '10px'; tip.style.top = 'auto'; }
524
+ }
525
+
526
+ /* ── Panel Open/Close ── */
527
+ function togglePanel() {
528
+ if (panelOpen) closePanel();
529
+ else openPanel();
530
+ }
531
+
532
+ function openPanel() {
533
+ if (closingTimer) { clearTimeout(closingTimer); closingTimer = null; }
534
+ var isTop = currentCorner.includes('top');
535
+ var isRight = currentCorner.includes('right');
536
+
537
+ panel.style.bottom = isTop ? 'auto' : '52px';
538
+ panel.style.top = isTop ? '52px' : 'auto';
539
+ panel.style.left = isRight ? 'auto' : '0';
540
+ panel.style.right = isRight ? '0' : 'auto';
541
+ panel.style.opacity = '';
542
+ panel.style.transform = '';
543
+ panel.style.animation = isTop ? '__nitron_slideDown 0.15s ease-out' : '__nitron_slideUp 0.15s ease-out';
544
+ panel.style.display = 'block';
545
+ panelOpen = true;
546
+ }
547
+
548
+ function closePanel() {
549
+ if (!panelOpen) return;
550
+ panelOpen = false;
551
+ panel.style.opacity = '0';
552
+ panel.style.transform = 'translateY(4px)';
553
+ closingTimer = setTimeout(function() {
554
+ closingTimer = null;
555
+ panel.style.display = 'none';
556
+ panel.style.opacity = '';
557
+ panel.style.transform = '';
558
+ }, 120);
559
+ }
560
+
561
+ /* ── Section Accordion ── */
562
+ document.querySelectorAll('.ndi-sec').forEach(function(sec) {
563
+ sec.addEventListener('click', function(e) {
564
+ if (e.target.closest('.ndi-tree-row') || e.target.closest('.ndi-tree-more')) return;
565
+ var name = this.dataset.section;
566
+ var details = document.getElementById('ndi-det-' + name);
567
+ if (!details) return;
568
+ var isOpen = this.classList.contains('active');
569
+ this.classList.toggle('active');
570
+
571
+ if (isOpen) {
572
+ details.style.maxHeight = '0';
573
+ }
574
+ else {
575
+ if (name === 'props' && !propsTreeBuilt && PROPS_DATA) {
576
+ buildPropsTree();
577
+ propsTreeBuilt = true;
578
+ }
579
+ details.style.maxHeight = details.scrollHeight + 'px';
580
+ setTimeout(function() { details.style.maxHeight = 'none'; }, 350);
581
+ }
582
+ });
583
+ });
584
+
585
+ /* ── Tree View Renderer ── */
586
+ function escHtml(s) {
587
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
588
+ }
589
+
590
+ function buildPropsTree() {
591
+ var el = document.getElementById('ndi-props-tree');
592
+ if (!el || !PROPS_DATA) return;
593
+
594
+ var html = '';
595
+ for (var i = 0; i < PROPS_DATA.sent.length; i++) {
596
+ var p = PROPS_DATA.sent[i];
597
+ html += renderNode(p.key, p.value, 0);
598
+ }
599
+
600
+ if (PROPS_DATA.filtered.length > 0) {
601
+ html += '<div class="ndi-tree-sep"><div class="ndi-tree-sep-line"></div><span class="ndi-tree-sep-text">Filtered</span><div class="ndi-tree-sep-line"></div></div>';
602
+ for (var i = 0; i < PROPS_DATA.filtered.length; i++) {
603
+ html += '<div class="ndi-tree-row filtered">';
604
+ html += '<span class="ndi-tree-arrow h">\u25B8</span>';
605
+ html += '<span class="ndi-tree-key">' + escHtml(PROPS_DATA.filtered[i]) + '</span>';
606
+ html += '<span class="ndi-tree-stripped">stripped</span>';
607
+ html += '</div>';
608
+ }
609
+ }
610
+
611
+ el.innerHTML = html;
612
+ }
613
+
614
+ function renderNode(key, value, depth) {
615
+ var indent = '';
616
+ for (var d = 0; d < depth; d++) indent += '<span class="ndi-tree-guide"></span>';
617
+
618
+ if (value === null) {
619
+ return '<div class="ndi-tree-row">' + indent + '<span class="ndi-tree-arrow h">\u25B8</span><span class="ndi-tree-key">' + escHtml(key) + '</span><span class="ndi-tree-colon">:</span><span class="ndi-tree-null">null</span></div>';
620
+ }
621
+
622
+ var type = typeof value;
623
+
624
+ if (type === 'string') {
625
+ var display = value.length > 120 ? escHtml(value.substring(0, 120)) + '...' : escHtml(value);
626
+ return '<div class="ndi-tree-row">' + indent + '<span class="ndi-tree-arrow h">\u25B8</span><span class="ndi-tree-key">' + escHtml(key) + '</span><span class="ndi-tree-colon">:</span><span class="ndi-tree-str">' + display + '</span></div>';
627
+ }
628
+
629
+ if (type === 'number') {
630
+ return '<div class="ndi-tree-row">' + indent + '<span class="ndi-tree-arrow h">\u25B8</span><span class="ndi-tree-key">' + escHtml(key) + '</span><span class="ndi-tree-colon">:</span><span class="ndi-tree-num">' + value + '</span></div>';
631
+ }
632
+
633
+ if (type === 'boolean') {
634
+ return '<div class="ndi-tree-row">' + indent + '<span class="ndi-tree-arrow h">\u25B8</span><span class="ndi-tree-key">' + escHtml(key) + '</span><span class="ndi-tree-colon">:</span><span class="ndi-tree-bool">' + value + '</span></div>';
635
+ }
636
+
637
+ if (Array.isArray(value)) {
638
+ var id = 'ndi-tc-' + (++moreCounter);
639
+ var html = '<div class="ndi-tree-row exp" data-children="' + id + '">' + indent + '<span class="ndi-tree-arrow">\u25B8</span><span class="ndi-tree-key">' + escHtml(key) + '</span><span class="ndi-tree-colon">:</span><span class="ndi-tree-badge">Array [' + value.length + ']</span></div>';
640
+ html += '<div class="ndi-tree-children" id="' + id + '" style="display:none">';
641
+
642
+ var showCount = Math.min(ARRAY_INITIAL, value.length);
643
+ for (var i = 0; i < showCount; i++) {
644
+ html += renderNode('[' + i + ']', value[i], depth + 1);
645
+ }
646
+
647
+ if (value.length > ARRAY_INITIAL) {
648
+ var moreId = 'ndi-more-' + (++moreCounter);
649
+ html += '<div class="ndi-tree-more" data-more="' + moreId + '" style="padding-left:' + ((depth + 1) * 20) + 'px"><span class="ndi-tree-more-dots">\u2022\u2022\u2022</span> ' + (value.length - ARRAY_INITIAL) + ' more items</div>';
650
+ html += '<div id="' + moreId + '" style="display:none">';
651
+ for (var i = ARRAY_INITIAL; i < value.length; i++) {
652
+ html += renderNode('[' + i + ']', value[i], depth + 1);
653
+ }
654
+ html += '</div>';
655
+ }
656
+
657
+ html += '</div>';
658
+ return html;
659
+ }
660
+
661
+ if (type === 'object') {
662
+ var keys = Object.keys(value);
663
+ var id = 'ndi-tc-' + (++moreCounter);
664
+ var html = '<div class="ndi-tree-row exp" data-children="' + id + '">' + indent + '<span class="ndi-tree-arrow">\u25B8</span><span class="ndi-tree-key">' + escHtml(key) + '</span><span class="ndi-tree-colon">:</span><span class="ndi-tree-badge">Object {' + keys.length + '}</span></div>';
665
+ html += '<div class="ndi-tree-children" id="' + id + '" style="display:none">';
666
+
667
+ for (var i = 0; i < keys.length; i++) {
668
+ html += renderNode(keys[i], value[keys[i]], depth + 1);
669
+ }
670
+
671
+ html += '</div>';
672
+ return html;
673
+ }
674
+
675
+ return '<div class="ndi-tree-row">' + indent + '<span class="ndi-tree-arrow h">\u25B8</span><span class="ndi-tree-key">' + escHtml(key) + '</span><span class="ndi-tree-colon">:</span><span class="ndi-tree-null">' + escHtml(String(value)) + '</span></div>';
676
+ }
677
+
678
+ /* ── Tree Event Delegation ── */
679
+ var treeEl = document.getElementById('ndi-props-tree');
680
+ if (treeEl) {
681
+ treeEl.addEventListener('click', function(e) {
682
+ var row = e.target.closest('.ndi-tree-row.exp');
683
+ if (row) {
684
+ var childrenId = row.dataset.children;
685
+ var children = document.getElementById(childrenId);
686
+ if (children) {
687
+ var isOpen = row.classList.contains('open');
688
+ row.classList.toggle('open');
689
+ children.style.display = isOpen ? 'none' : '';
690
+ var det = document.getElementById('ndi-det-props');
691
+ if (det) det.style.maxHeight = 'none';
692
+ }
693
+ return;
694
+ }
695
+
696
+ var more = e.target.closest('.ndi-tree-more');
697
+ if (more) {
698
+ var hiddenId = more.dataset.more;
699
+ var hidden = document.getElementById(hiddenId);
700
+ if (hidden) {
701
+ hidden.style.display = '';
702
+ more.style.display = 'none';
703
+ var det = document.getElementById('ndi-det-props');
704
+ if (det) det.style.maxHeight = 'none';
705
+ }
706
+ }
707
+ });
708
+ }
709
+
710
+ /* ── HMR Status ── */
711
+ function updateHmr() {
712
+ var connected = window.__nitron_hmr_connected__;
713
+ if (dot) dot.style.background = connected ? '#4ade80' : '#ef4444';
714
+ if (hmrDot) hmrDot.style.background = connected ? '#4ade80' : '#ef4444';
715
+ if (hmrTxt) hmrTxt.textContent = connected ? 'HMR' : 'Disconnected';
716
+ if (hmrBadge) {
717
+ hmrBadge.style.color = connected ? '#4ade80' : '#ef4444';
718
+ hmrBadge.style.background = connected ? 'rgba(74,222,128,0.06)' : 'rgba(239,68,68,0.06)';
719
+ hmrBadge.style.borderColor = connected ? 'rgba(74,222,128,0.12)' : 'rgba(239,68,68,0.12)';
720
+ }
721
+ if (footDot) footDot.className = 'ndi-foot-dot ' + (connected ? 'ok' : 'err');
722
+ if (footHmr) footHmr.textContent = connected ? 'HMR' : 'Disconnected';
723
+ }
724
+
725
+ if (typeof io !== 'undefined') {
726
+ var hmrInterval = setInterval(function() {
727
+ if (!container.isConnected) { clearInterval(hmrInterval); return; }
728
+ updateHmr();
729
+ }, 1000);
730
+ updateHmr();
731
+ }
732
+ else {
733
+ if (hmrTxt) hmrTxt.textContent = 'N/A';
734
+ if (hmrBadge) { hmrBadge.style.color = 'var(--yellow)'; hmrBadge.style.borderColor = 'rgba(251,191,36,0.12)'; }
735
+ if (footDot) footDot.className = 'ndi-foot-dot warn';
736
+ if (footHmr) footHmr.textContent = 'N/A';
737
+ }
738
+ })();
739
+ </script>`;
740
+ }
741
+ }
742
+
743
+ export default DevIndicator;
744
+
745
+
746
+ // ─────────────────────────────────────────────────────────────────────────────
747
+ // Helper Functions
748
+ // ─────────────────────────────────────────────────────────────────────────────
749
+
750
+ function esc(str) {
751
+ if (!str) return "";
752
+ return String(str).replace(/[&<>"']/g, c => ESC[c]);
753
+ }
754
+
755
+ function buildTimingSummary(devData) {
756
+ const middlewareTotal = (devData.middlewareTiming || []).reduce((s, m) => s + m.duration, 0);
757
+ const controller = (devData.controllerStart && devData.renderStart)
758
+ ? (devData.renderStart - devData.controllerStart)
759
+ : (devData.controllerDuration || 0);
760
+ const render = devData.renderDuration || 0;
761
+ const total = devData.startTime ? performance.now() - devData.startTime : (middlewareTotal + controller + render);
762
+
763
+ if (!isFinite(total) || total <= 0) {
764
+ return { total: "0.0", middlewareTotal: "0.0", controller: "0.0", render: "0.0", mwPct: "0", ctrlPct: "0", renderPct: "0", otherPct: "0" };
765
+ }
766
+
767
+ const mwPct = (middlewareTotal / total * 100);
768
+ const ctrlPct = (controller / total * 100);
769
+ const renderPct = (render / total * 100);
770
+ const otherPct = Math.max(0, 100 - mwPct - ctrlPct - renderPct);
771
+
772
+ return {
773
+ total: total.toFixed(1),
774
+ middlewareTotal: middlewareTotal.toFixed(1),
775
+ controller: controller.toFixed(1),
776
+ render: render.toFixed(1),
777
+ mwPct: mwPct.toFixed(1),
778
+ ctrlPct: ctrlPct.toFixed(1),
779
+ renderPct: renderPct.toFixed(1),
780
+ otherPct: otherPct.toFixed(1)
781
+ };
782
+ }
783
+
784
+ function buildPropsSummary(devData) {
785
+ const rawKeys = Object.keys(devData.rawProps || {});
786
+ const usageKeys = devData.propUsage ? Object.keys(devData.propUsage) : rawKeys;
787
+ const removedKeys = rawKeys.filter(k => !usageKeys.includes(k));
788
+
789
+ return {
790
+ sent: rawKeys.length,
791
+ removed: removedKeys.length,
792
+ keys: rawKeys,
793
+ removedKeys
794
+ };
795
+ }
796
+
797
+ function buildPropsTreeData(devData) {
798
+ const raw = devData.rawProps || {};
799
+ const usageKeys = devData.propUsage ? Object.keys(devData.propUsage) : null;
800
+ const allKeys = Object.keys(raw);
801
+
802
+ const sent = [];
803
+ const filtered = [];
804
+
805
+ for (const key of allKeys) {
806
+ if (!usageKeys || usageKeys.includes(key)) {
807
+ sent.push({ key, value: raw[key] });
808
+ }
809
+ else {
810
+ filtered.push(key);
811
+ }
812
+ }
813
+
814
+ return { sent, filtered };
815
+ }
816
+
817
+ function safeJsonEmbed(data) {
818
+ try {
819
+ const seen = new WeakSet();
820
+ const json = JSON.stringify(data, function(key, value) {
821
+ if (typeof value === "object" && value !== null) {
822
+ if (seen.has(value)) return "[Circular]";
823
+ seen.add(value);
824
+ }
825
+ if (typeof value === "string" && value.length > 200) return value.substring(0, 200) + "...";
826
+ if (typeof value === "function") return undefined;
827
+ if (typeof value === "symbol") return undefined;
828
+ if (typeof value === "bigint") return String(value);
829
+ return value;
830
+ });
831
+ return json.replace(/<\//g, "<\\/");
832
+ }
833
+ catch (e) {
834
+ return "null";
835
+ }
836
+ }