@flight-framework/devtools 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,18 @@
1
1
  # @flight-framework/devtools
2
2
 
3
- Developer tools for Flight Framework debugging and inspection.
3
+ Developer tools for Flight Framework. Inspect routes, debug server actions, and monitor performance.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Quick Start](#quick-start)
9
+ - [Features](#features)
10
+ - [Configuration](#configuration)
11
+ - [Browser Extension](#browser-extension)
12
+ - [API](#api)
13
+ - [License](#license)
14
+
15
+ ---
4
16
 
5
17
  ## Installation
6
18
 
@@ -8,7 +20,9 @@ Developer tools for Flight Framework debugging and inspection.
8
20
  npm install -D @flight-framework/devtools
9
21
  ```
10
22
 
11
- ## Usage
23
+ ---
24
+
25
+ ## Quick Start
12
26
 
13
27
  ```typescript
14
28
  // flight.config.ts
@@ -22,28 +36,185 @@ export default defineConfig({
22
36
  });
23
37
  ```
24
38
 
39
+ Access the devtools panel at `/__devtools` in development mode.
40
+
41
+ ---
42
+
25
43
  ## Features
26
44
 
27
- - Route inspection
28
- - Component tree visualization
29
- - Server action debugging
30
- - Cache inspection
31
- - Performance metrics
32
- - Network requests
45
+ ### Route Inspector
46
+ - View all registered routes
47
+ - See route parameters and patterns
48
+ - Test route matching
33
49
 
34
- ## Browser Extension
50
+ ### Component Tree
51
+ - Visualize component hierarchy
52
+ - Inspect props and state
53
+ - Highlight rendering
54
+
55
+ ### Server Actions
56
+ - Log all server action calls
57
+ - View request/response payloads
58
+ - Measure execution time
59
+
60
+ ### Cache Inspector
61
+ - View cached entries
62
+ - Inspect TTL and hit rates
63
+ - Manually invalidate entries
64
+
65
+ ### Network Monitor
66
+ - Track API requests
67
+ - View request/response headers
68
+ - Measure response times
69
+
70
+ ### Performance
71
+ - Core Web Vitals (LCP, FID, CLS)
72
+ - Hydration timing
73
+ - Bundle size analysis
74
+
75
+ ---
76
+
77
+ ## Contributing
78
+
79
+ See the main [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.
80
+
81
+ ---
82
+
83
+ ## New in V2: Enhanced Panels
35
84
 
36
- Install the Flight DevTools browser extension for Chrome or Firefox to view the devtools panel.
85
+ ### Hydration Panel
86
+
87
+ Track island hydration timing and status:
88
+
89
+ ```typescript
90
+ import { subscribeToHydration, setupHydrationTracking } from '@flight-framework/devtools/hydration';
91
+
92
+ // Auto-track hydration events
93
+ setupHydrationTracking();
94
+
95
+ // Subscribe to updates
96
+ subscribeToHydration((islands, metrics) => {
97
+ console.log(`Hydrated: ${metrics.hydratedCount}/${metrics.totalIslands}`);
98
+ console.log(`Avg time: ${metrics.averageHydrationTime}ms`);
99
+ });
100
+ ```
101
+
102
+ ### Bundle Panel
103
+
104
+ Monitor chunk loading and sizes:
105
+
106
+ ```typescript
107
+ import { subscribeToBundles, setupBundleTracking } from '@flight-framework/devtools/bundle';
108
+
109
+ // Auto-track via Performance API
110
+ setupBundleTracking();
111
+
112
+ // Subscribe to updates
113
+ subscribeToBundles((chunks, metrics) => {
114
+ console.log(`Total size: ${metrics.totalSize} bytes`);
115
+ console.log(`Loaded: ${metrics.loadedChunks} chunks`);
116
+ });
117
+ ```
118
+
119
+ ### Enhanced Cache Panel
120
+
121
+ Advanced cache inspection with tag support:
122
+
123
+ ```typescript
124
+ import { subscribeToCache, invalidateByTag } from '@flight-framework/devtools/cache';
125
+
126
+ // Subscribe to cache updates
127
+ subscribeToCache((entries, metrics) => {
128
+ console.log(`Hit rate: ${metrics.hitRate}%`);
129
+ console.log(`Tags: ${metrics.uniqueTags.join(', ')}`);
130
+ });
131
+
132
+ // Invalidate by tag
133
+ invalidateByTag('user-data');
134
+ ```
135
+
136
+ ---
37
137
 
38
138
  ## Configuration
39
139
 
40
140
  ```typescript
41
141
  devtools({
142
+ // Enable in development only (default)
42
143
  enabled: process.env.NODE_ENV !== 'production',
144
+
145
+ // Devtools server port
43
146
  port: 9229,
147
+
148
+ // Open devtools automatically
149
+ open: false,
150
+
151
+ // Features to enable
152
+ features: {
153
+ routes: true,
154
+ components: true,
155
+ actions: true,
156
+ cache: true,
157
+ network: true,
158
+ performance: true,
159
+ },
160
+
161
+ // Overlay position
162
+ position: 'bottom-right',
163
+ });
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Browser Extension
169
+
170
+ Install the Flight DevTools browser extension for an integrated debugging experience:
171
+
172
+ - **Chrome**: [Flight DevTools](https://chrome.google.com/webstore/...)
173
+ - **Firefox**: [Flight DevTools](https://addons.mozilla.org/...)
174
+
175
+ The extension adds a "Flight" panel to your browser's developer tools.
176
+
177
+ ---
178
+
179
+ ## API
180
+
181
+ ### Programmatic Access
182
+
183
+ ```typescript
184
+ import { getDevtools } from '@flight-framework/devtools';
185
+
186
+ const devtools = getDevtools();
187
+
188
+ // Log custom event
189
+ devtools.log('custom', { message: 'Hello' });
190
+
191
+ // Add custom panel
192
+ devtools.addPanel('my-panel', {
193
+ title: 'My Panel',
194
+ render: () => '<div>Custom content</div>',
44
195
  });
45
196
  ```
46
197
 
198
+ ### React Hook
199
+
200
+ ```tsx
201
+ import { useDevtools } from '@flight-framework/devtools/react';
202
+
203
+ function MyComponent() {
204
+ const { log, measure } = useDevtools();
205
+
206
+ const handleClick = () => {
207
+ const end = measure('click-handler');
208
+ // ... do work
209
+ end();
210
+ };
211
+
212
+ return <button onClick={handleClick}>Click</button>;
213
+ }
214
+ ```
215
+
216
+ ---
217
+
47
218
  ## License
48
219
 
49
220
  MIT
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @flight-framework/devtools - Bundle Panel
3
+ *
4
+ * Visualizes bundle composition within DevTools.
5
+ * Shows chunk breakdown, sizes, and loading status.
6
+ */
7
+ /**
8
+ * Information about a loaded chunk
9
+ */
10
+ export interface ChunkInfo {
11
+ /** Chunk name/identifier */
12
+ name: string;
13
+ /** Chunk URL */
14
+ url: string;
15
+ /** Size in bytes (uncompressed) */
16
+ size: number;
17
+ /** Gzip size if available */
18
+ gzipSize?: number;
19
+ /** Loading status */
20
+ status: 'pending' | 'loading' | 'loaded' | 'error';
21
+ /** Load time in ms */
22
+ loadTime?: number;
23
+ /** Whether this is an entry chunk */
24
+ isEntry: boolean;
25
+ /** Whether this is a dynamic import */
26
+ isDynamic: boolean;
27
+ /** Parse time if available */
28
+ parseTime?: number;
29
+ /** Modules in this chunk */
30
+ modules?: string[];
31
+ }
32
+ /**
33
+ * Bundle metrics
34
+ */
35
+ export interface BundleMetrics {
36
+ /** Total number of chunks */
37
+ totalChunks: number;
38
+ /** Number of loaded chunks */
39
+ loadedChunks: number;
40
+ /** Total size of all chunks */
41
+ totalSize: number;
42
+ /** Total gzip size */
43
+ totalGzipSize: number;
44
+ /** Entry chunks size */
45
+ entrySize: number;
46
+ /** Dynamic chunks size (lazy loaded) */
47
+ dynamicSize: number;
48
+ /** Total load time for all chunks */
49
+ totalLoadTime: number;
50
+ /** Largest chunk */
51
+ largestChunk?: ChunkInfo;
52
+ }
53
+ /**
54
+ * Register a chunk
55
+ */
56
+ export declare function registerChunk(info: ChunkInfo): void;
57
+ /**
58
+ * Update chunk status
59
+ */
60
+ export declare function updateChunkStatus(name: string, status: ChunkInfo['status'], loadTime?: number): void;
61
+ /**
62
+ * Subscribe to bundle updates
63
+ */
64
+ export declare function subscribeToBundles(callback: (chunks: ChunkInfo[], metrics: BundleMetrics) => void): () => void;
65
+ /**
66
+ * Get current bundle state
67
+ */
68
+ export declare function getBundleState(): {
69
+ chunks: ChunkInfo[];
70
+ metrics: BundleMetrics;
71
+ };
72
+ /**
73
+ * Clear all tracked chunks
74
+ */
75
+ export declare function clearBundleState(): void;
76
+ /**
77
+ * Setup automatic chunk tracking via Performance API
78
+ */
79
+ export declare function setupBundleTracking(): void;
80
+ /**
81
+ * Generate HTML for the bundle panel
82
+ */
83
+ export declare function renderBundlePanel(chunksList: ChunkInfo[], metrics: BundleMetrics): string;
84
+ /**
85
+ * CSS styles for the bundle panel
86
+ */
87
+ export declare const bundlePanelStyles = "\n.flight-dt-bundle-panel {\n padding: 8px;\n}\n\n.flight-dt-bundle-summary {\n display: flex;\n gap: 16px;\n padding: 8px;\n background: rgba(255, 255, 255, 0.05);\n border-radius: 6px;\n margin-bottom: 12px;\n}\n\n.flight-dt-bundle-breakdown {\n margin-bottom: 12px;\n}\n\n.flight-dt-breakdown-row {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 6px;\n}\n\n.flight-dt-breakdown-label {\n width: 60px;\n font-size: 11px;\n color: #888;\n}\n\n.flight-dt-breakdown-bar {\n flex: 1;\n height: 8px;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 4px;\n overflow: hidden;\n}\n\n.flight-dt-breakdown-fill {\n height: 100%;\n border-radius: 4px;\n}\n\n.flight-dt-breakdown-fill--entry {\n background: #3b82f6;\n}\n\n.flight-dt-breakdown-fill--dynamic {\n background: #8b5cf6;\n}\n\n.flight-dt-breakdown-value {\n width: 70px;\n text-align: right;\n font-size: 11px;\n color: #888;\n font-family: monospace;\n}\n\n.flight-dt-chunks-list {\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-height: 200px;\n overflow-y: auto;\n}\n\n.flight-dt-chunk {\n padding: 6px 8px;\n background: rgba(255, 255, 255, 0.03);\n border-radius: 4px;\n border-left: 2px solid #666;\n}\n\n.flight-dt-chunk--entry {\n border-left-color: #3b82f6;\n}\n\n.flight-dt-chunk--dynamic {\n border-left-color: #8b5cf6;\n}\n\n.flight-dt-chunk-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 4px;\n}\n\n.flight-dt-chunk-name {\n font-family: monospace;\n font-size: 11px;\n color: #e0e0e0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 200px;\n}\n\n.flight-dt-chunk-size {\n font-family: monospace;\n font-size: 11px;\n color: #888;\n}\n\n.flight-dt-chunk-bar {\n height: 4px;\n background: rgba(255, 255, 255, 0.1);\n border-radius: 2px;\n overflow: hidden;\n}\n\n.flight-dt-chunk-bar-fill {\n height: 100%;\n background: #22c55e;\n border-radius: 2px;\n}\n\n.flight-dt-chunk-time {\n font-size: 10px;\n color: #666;\n font-family: monospace;\n}\n\n.flight-dt-chunks-more {\n text-align: center;\n font-size: 11px;\n color: #666;\n padding: 8px;\n}\n";
88
+ //# sourceMappingURL=bundle-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-panel.d.ts","sourceRoot":"","sources":["../src/bundle-panel.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IAEb,gBAAgB;IAChB,GAAG,EAAE,MAAM,CAAC;IAEZ,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IAEb,6BAA6B;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,qBAAqB;IACrB,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IAEnD,sBAAsB;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IAEjB,uCAAuC;IACvC,SAAS,EAAE,OAAO,CAAC;IAEnB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC1B,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IAEpB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;IAErB,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;IAElB,sBAAsB;IACtB,aAAa,EAAE,MAAM,CAAC;IAEtB,wBAAwB;IACxB,SAAS,EAAE,MAAM,CAAC;IAElB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IAEpB,qCAAqC;IACrC,aAAa,EAAE,MAAM,CAAC;IAEtB,oBAAoB;IACpB,YAAY,CAAC,EAAE,SAAS,CAAC;CAC5B;AAsCD;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAGnD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,EAC3B,QAAQ,CAAC,EAAE,MAAM,GAClB,IAAI,CAWN;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,aAAa,KAAK,IAAI,GAChE,MAAM,IAAI,CAOZ;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE,aAAa,CAAC;CAC1B,CAMA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC;AAMD;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CA8C1C;AAmCD;;GAEG;AACH,wBAAgB,iBAAiB,CAC7B,UAAU,EAAE,SAAS,EAAE,EACvB,OAAO,EAAE,aAAa,GACvB,MAAM,CAiER;AASD;;GAEG;AACH,eAAO,MAAM,iBAAiB,kzEAmI7B,CAAC"}
@@ -0,0 +1,371 @@
1
+ /**
2
+ * @flight-framework/devtools - Bundle Panel
3
+ *
4
+ * Visualizes bundle composition within DevTools.
5
+ * Shows chunk breakdown, sizes, and loading status.
6
+ */
7
+ // ============================================================================
8
+ // State Management
9
+ // ============================================================================
10
+ const chunks = new Map();
11
+ const subscribers = new Set();
12
+ function notify() {
13
+ const chunksList = Array.from(chunks.values());
14
+ const metrics = calculateMetrics(chunksList);
15
+ for (const callback of subscribers) {
16
+ callback(chunksList, metrics);
17
+ }
18
+ }
19
+ function calculateMetrics(chunksList) {
20
+ const loaded = chunksList.filter(c => c.status === 'loaded');
21
+ const sorted = [...loaded].sort((a, b) => b.size - a.size);
22
+ return {
23
+ totalChunks: chunksList.length,
24
+ loadedChunks: loaded.length,
25
+ totalSize: chunksList.reduce((sum, c) => sum + c.size, 0),
26
+ totalGzipSize: chunksList.reduce((sum, c) => sum + (c.gzipSize || c.size * 0.3), 0),
27
+ entrySize: chunksList.filter(c => c.isEntry).reduce((sum, c) => sum + c.size, 0),
28
+ dynamicSize: chunksList.filter(c => c.isDynamic).reduce((sum, c) => sum + c.size, 0),
29
+ totalLoadTime: loaded.reduce((sum, c) => sum + (c.loadTime || 0), 0),
30
+ largestChunk: sorted[0],
31
+ };
32
+ }
33
+ // ============================================================================
34
+ // Public API
35
+ // ============================================================================
36
+ /**
37
+ * Register a chunk
38
+ */
39
+ export function registerChunk(info) {
40
+ chunks.set(info.name, info);
41
+ notify();
42
+ }
43
+ /**
44
+ * Update chunk status
45
+ */
46
+ export function updateChunkStatus(name, status, loadTime) {
47
+ const chunk = chunks.get(name);
48
+ if (!chunk)
49
+ return;
50
+ chunk.status = status;
51
+ if (loadTime !== undefined) {
52
+ chunk.loadTime = loadTime;
53
+ }
54
+ chunks.set(name, chunk);
55
+ notify();
56
+ }
57
+ /**
58
+ * Subscribe to bundle updates
59
+ */
60
+ export function subscribeToBundles(callback) {
61
+ subscribers.add(callback);
62
+ const chunksList = Array.from(chunks.values());
63
+ callback(chunksList, calculateMetrics(chunksList));
64
+ return () => subscribers.delete(callback);
65
+ }
66
+ /**
67
+ * Get current bundle state
68
+ */
69
+ export function getBundleState() {
70
+ const chunksList = Array.from(chunks.values());
71
+ return {
72
+ chunks: chunksList,
73
+ metrics: calculateMetrics(chunksList),
74
+ };
75
+ }
76
+ /**
77
+ * Clear all tracked chunks
78
+ */
79
+ export function clearBundleState() {
80
+ chunks.clear();
81
+ notify();
82
+ }
83
+ // ============================================================================
84
+ // Auto-instrumentation
85
+ // ============================================================================
86
+ /**
87
+ * Setup automatic chunk tracking via Performance API
88
+ */
89
+ export function setupBundleTracking() {
90
+ if (typeof window === 'undefined')
91
+ return;
92
+ // Track dynamically loaded scripts
93
+ const observer = new PerformanceObserver((list) => {
94
+ for (const perfEntry of list.getEntries()) {
95
+ const entry = perfEntry;
96
+ if (perfEntry.entryType === 'resource' && entry.initiatorType === 'script') {
97
+ const name = extractChunkName(perfEntry.name);
98
+ registerChunk({
99
+ name,
100
+ url: perfEntry.name,
101
+ size: entry.transferSize || entry.encodedBodySize || 0,
102
+ gzipSize: entry.encodedBodySize || undefined,
103
+ status: 'loaded',
104
+ loadTime: perfEntry.duration,
105
+ isEntry: isEntryChunk(perfEntry.name),
106
+ isDynamic: isDynamicChunk(perfEntry.name),
107
+ });
108
+ }
109
+ }
110
+ });
111
+ observer.observe({ entryTypes: ['resource'] });
112
+ // Track already-loaded scripts
113
+ if (performance.getEntriesByType) {
114
+ const resources = performance.getEntriesByType('resource');
115
+ for (const entry of resources) {
116
+ if (entry.initiatorType === 'script') {
117
+ const name = extractChunkName(entry.name);
118
+ registerChunk({
119
+ name,
120
+ url: entry.name,
121
+ size: entry.transferSize || entry.encodedBodySize || 0,
122
+ gzipSize: entry.encodedBodySize || undefined,
123
+ status: 'loaded',
124
+ loadTime: entry.duration,
125
+ isEntry: isEntryChunk(entry.name),
126
+ isDynamic: isDynamicChunk(entry.name),
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ function extractChunkName(url) {
133
+ try {
134
+ const pathname = new URL(url).pathname;
135
+ const segments = pathname.split('/');
136
+ return segments[segments.length - 1] || pathname;
137
+ }
138
+ catch {
139
+ return url;
140
+ }
141
+ }
142
+ function isEntryChunk(url) {
143
+ const name = extractChunkName(url).toLowerCase();
144
+ return name.includes('index') || name.includes('entry') || name.includes('main');
145
+ }
146
+ function isDynamicChunk(url) {
147
+ const name = extractChunkName(url).toLowerCase();
148
+ return name.includes('chunk') || name.includes('async') || /[a-f0-9]{8}/.test(name);
149
+ }
150
+ // ============================================================================
151
+ // Panel Rendering
152
+ // ============================================================================
153
+ /**
154
+ * Format bytes for display
155
+ */
156
+ function formatBytes(bytes) {
157
+ if (bytes < 1024)
158
+ return `${bytes} B`;
159
+ if (bytes < 1024 * 1024)
160
+ return `${(bytes / 1024).toFixed(1)} KB`;
161
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
162
+ }
163
+ /**
164
+ * Generate HTML for the bundle panel
165
+ */
166
+ export function renderBundlePanel(chunksList, metrics) {
167
+ const sortedChunks = [...chunksList].sort((a, b) => b.size - a.size);
168
+ // Calculate bar widths relative to largest chunk
169
+ const maxSize = metrics.largestChunk?.size || 1;
170
+ return `
171
+ <div class="flight-dt-bundle-panel">
172
+ <div class="flight-dt-bundle-summary">
173
+ <div class="flight-dt-metric">
174
+ <span class="flight-dt-metric-value">${formatBytes(metrics.totalSize)}</span>
175
+ <span class="flight-dt-metric-label">Total Size</span>
176
+ </div>
177
+ <div class="flight-dt-metric">
178
+ <span class="flight-dt-metric-value">${formatBytes(metrics.totalGzipSize)}</span>
179
+ <span class="flight-dt-metric-label">Gzip Size</span>
180
+ </div>
181
+ <div class="flight-dt-metric">
182
+ <span class="flight-dt-metric-value">${metrics.totalChunks}</span>
183
+ <span class="flight-dt-metric-label">Chunks</span>
184
+ </div>
185
+ <div class="flight-dt-metric">
186
+ <span class="flight-dt-metric-value">${metrics.totalLoadTime.toFixed(0)}ms</span>
187
+ <span class="flight-dt-metric-label">Load Time</span>
188
+ </div>
189
+ </div>
190
+
191
+ <div class="flight-dt-bundle-breakdown">
192
+ <div class="flight-dt-breakdown-row">
193
+ <span class="flight-dt-breakdown-label">Entry</span>
194
+ <div class="flight-dt-breakdown-bar">
195
+ <div class="flight-dt-breakdown-fill flight-dt-breakdown-fill--entry"
196
+ style="width: ${(metrics.entrySize / metrics.totalSize) * 100}%"></div>
197
+ </div>
198
+ <span class="flight-dt-breakdown-value">${formatBytes(metrics.entrySize)}</span>
199
+ </div>
200
+ <div class="flight-dt-breakdown-row">
201
+ <span class="flight-dt-breakdown-label">Dynamic</span>
202
+ <div class="flight-dt-breakdown-bar">
203
+ <div class="flight-dt-breakdown-fill flight-dt-breakdown-fill--dynamic"
204
+ style="width: ${(metrics.dynamicSize / metrics.totalSize) * 100}%"></div>
205
+ </div>
206
+ <span class="flight-dt-breakdown-value">${formatBytes(metrics.dynamicSize)}</span>
207
+ </div>
208
+ </div>
209
+
210
+ <div class="flight-dt-chunks-list">
211
+ ${sortedChunks.slice(0, 10).map(chunk => `
212
+ <div class="flight-dt-chunk ${chunk.isEntry ? 'flight-dt-chunk--entry' : ''} ${chunk.isDynamic ? 'flight-dt-chunk--dynamic' : ''}">
213
+ <div class="flight-dt-chunk-header">
214
+ <span class="flight-dt-chunk-name" title="${escapeHtml(chunk.url)}">${escapeHtml(chunk.name)}</span>
215
+ <span class="flight-dt-chunk-size">${formatBytes(chunk.size)}</span>
216
+ </div>
217
+ <div class="flight-dt-chunk-bar">
218
+ <div class="flight-dt-chunk-bar-fill" style="width: ${(chunk.size / maxSize) * 100}%"></div>
219
+ </div>
220
+ ${chunk.loadTime ? `<span class="flight-dt-chunk-time">${chunk.loadTime.toFixed(0)}ms</span>` : ''}
221
+ </div>
222
+ `).join('')}
223
+ ${sortedChunks.length > 10 ? `<div class="flight-dt-chunks-more">+ ${sortedChunks.length - 10} more chunks</div>` : ''}
224
+ </div>
225
+
226
+ ${sortedChunks.length === 0 ? '<div class="flight-dt-empty">No chunks detected</div>' : ''}
227
+ </div>
228
+ `;
229
+ }
230
+ function escapeHtml(str) {
231
+ return str
232
+ .replace(/&/g, '&amp;')
233
+ .replace(/</g, '&lt;')
234
+ .replace(/>/g, '&gt;');
235
+ }
236
+ /**
237
+ * CSS styles for the bundle panel
238
+ */
239
+ export const bundlePanelStyles = `
240
+ .flight-dt-bundle-panel {
241
+ padding: 8px;
242
+ }
243
+
244
+ .flight-dt-bundle-summary {
245
+ display: flex;
246
+ gap: 16px;
247
+ padding: 8px;
248
+ background: rgba(255, 255, 255, 0.05);
249
+ border-radius: 6px;
250
+ margin-bottom: 12px;
251
+ }
252
+
253
+ .flight-dt-bundle-breakdown {
254
+ margin-bottom: 12px;
255
+ }
256
+
257
+ .flight-dt-breakdown-row {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 8px;
261
+ margin-bottom: 6px;
262
+ }
263
+
264
+ .flight-dt-breakdown-label {
265
+ width: 60px;
266
+ font-size: 11px;
267
+ color: #888;
268
+ }
269
+
270
+ .flight-dt-breakdown-bar {
271
+ flex: 1;
272
+ height: 8px;
273
+ background: rgba(255, 255, 255, 0.1);
274
+ border-radius: 4px;
275
+ overflow: hidden;
276
+ }
277
+
278
+ .flight-dt-breakdown-fill {
279
+ height: 100%;
280
+ border-radius: 4px;
281
+ }
282
+
283
+ .flight-dt-breakdown-fill--entry {
284
+ background: #3b82f6;
285
+ }
286
+
287
+ .flight-dt-breakdown-fill--dynamic {
288
+ background: #8b5cf6;
289
+ }
290
+
291
+ .flight-dt-breakdown-value {
292
+ width: 70px;
293
+ text-align: right;
294
+ font-size: 11px;
295
+ color: #888;
296
+ font-family: monospace;
297
+ }
298
+
299
+ .flight-dt-chunks-list {
300
+ display: flex;
301
+ flex-direction: column;
302
+ gap: 6px;
303
+ max-height: 200px;
304
+ overflow-y: auto;
305
+ }
306
+
307
+ .flight-dt-chunk {
308
+ padding: 6px 8px;
309
+ background: rgba(255, 255, 255, 0.03);
310
+ border-radius: 4px;
311
+ border-left: 2px solid #666;
312
+ }
313
+
314
+ .flight-dt-chunk--entry {
315
+ border-left-color: #3b82f6;
316
+ }
317
+
318
+ .flight-dt-chunk--dynamic {
319
+ border-left-color: #8b5cf6;
320
+ }
321
+
322
+ .flight-dt-chunk-header {
323
+ display: flex;
324
+ justify-content: space-between;
325
+ align-items: center;
326
+ margin-bottom: 4px;
327
+ }
328
+
329
+ .flight-dt-chunk-name {
330
+ font-family: monospace;
331
+ font-size: 11px;
332
+ color: #e0e0e0;
333
+ overflow: hidden;
334
+ text-overflow: ellipsis;
335
+ white-space: nowrap;
336
+ max-width: 200px;
337
+ }
338
+
339
+ .flight-dt-chunk-size {
340
+ font-family: monospace;
341
+ font-size: 11px;
342
+ color: #888;
343
+ }
344
+
345
+ .flight-dt-chunk-bar {
346
+ height: 4px;
347
+ background: rgba(255, 255, 255, 0.1);
348
+ border-radius: 2px;
349
+ overflow: hidden;
350
+ }
351
+
352
+ .flight-dt-chunk-bar-fill {
353
+ height: 100%;
354
+ background: #22c55e;
355
+ border-radius: 2px;
356
+ }
357
+
358
+ .flight-dt-chunk-time {
359
+ font-size: 10px;
360
+ color: #666;
361
+ font-family: monospace;
362
+ }
363
+
364
+ .flight-dt-chunks-more {
365
+ text-align: center;
366
+ font-size: 11px;
367
+ color: #666;
368
+ padding: 8px;
369
+ }
370
+ `;
371
+ //# sourceMappingURL=bundle-panel.js.map