shokupan 0.10.4 → 0.11.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.
- package/dist/{analyzer-CKLGLFtx.cjs → analyzer-BAhvpNY_.cjs} +2 -7
- package/dist/{analyzer-CKLGLFtx.cjs.map → analyzer-BAhvpNY_.cjs.map} +1 -1
- package/dist/{analyzer-BqIe1p0R.js → analyzer-CnKnQ5KV.js} +3 -8
- package/dist/{analyzer-BqIe1p0R.js.map → analyzer-CnKnQ5KV.js.map} +1 -1
- package/dist/{analyzer.impl-D9Yi1Hax.cjs → analyzer.impl-CfpMu4-g.cjs} +586 -40
- package/dist/analyzer.impl-CfpMu4-g.cjs.map +1 -0
- package/dist/{analyzer.impl-CV6W1Eq7.js → analyzer.impl-DCiqlXI5.js} +586 -40
- package/dist/analyzer.impl-DCiqlXI5.js.map +1 -0
- package/dist/cli.cjs +206 -18
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +206 -18
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +6 -1
- package/dist/index.cjs +2405 -1008
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2402 -1006
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/api-explorer/static/explorer-client.mjs +423 -30
- package/dist/plugins/application/api-explorer/static/style.css +351 -10
- package/dist/plugins/application/api-explorer/static/theme.css +7 -2
- package/dist/plugins/application/asyncapi/generator.d.ts +4 -0
- package/dist/plugins/application/asyncapi/static/asyncapi-client.mjs +154 -22
- package/dist/plugins/application/asyncapi/static/style.css +24 -8
- package/dist/plugins/application/dashboard/fetch-interceptor.d.ts +107 -0
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +38 -2
- package/dist/plugins/application/dashboard/plugin.d.ts +44 -1
- package/dist/plugins/application/dashboard/static/charts.js +127 -62
- package/dist/plugins/application/dashboard/static/client.js +160 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +167 -56
- package/dist/plugins/application/dashboard/static/reactflow.css +20 -10
- package/dist/plugins/application/dashboard/static/registry.js +112 -8
- package/dist/plugins/application/dashboard/static/requests.js +868 -58
- package/dist/plugins/application/dashboard/static/styles.css +186 -14
- package/dist/plugins/application/dashboard/static/tabs.js +44 -9
- package/dist/plugins/application/dashboard/static/theme.css +7 -2
- package/dist/plugins/application/openapi/analyzer.impl.d.ts +61 -1
- package/dist/plugins/application/openapi/openapi.d.ts +3 -0
- package/dist/plugins/application/shared/ast-utils.d.ts +7 -0
- package/dist/router.d.ts +55 -16
- package/dist/shokupan.d.ts +7 -2
- package/dist/util/adapter/adapters.d.ts +19 -0
- package/dist/util/adapter/filesystem.d.ts +20 -0
- package/dist/util/controller-scanner.d.ts +4 -0
- package/dist/util/cpu-monitor.d.ts +2 -0
- package/dist/util/middleware-tracker.d.ts +10 -0
- package/dist/util/types.d.ts +37 -0
- package/package.json +5 -5
- package/dist/analyzer.impl-CV6W1Eq7.js.map +0 -1
- package/dist/analyzer.impl-D9Yi1Hax.cjs.map +0 -1
- package/dist/http-server-BEMPIs33.cjs +0 -85
- package/dist/http-server-BEMPIs33.cjs.map +0 -1
- package/dist/http-server-CCeagTyU.js +0 -68
- package/dist/http-server-CCeagTyU.js.map +0 -1
- package/dist/plugins/application/dashboard/static/poll.js +0 -146
|
@@ -40,6 +40,16 @@ function handleHashNavigation() {
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Check if it's a middleware view
|
|
44
|
+
if (hash.startsWith('middleware-')) {
|
|
45
|
+
const middlewareId = hash.replace('middleware-', '');
|
|
46
|
+
const middleware = explorerData.middlewareRegistry?.[middlewareId];
|
|
47
|
+
if (middleware) {
|
|
48
|
+
renderMiddlewareView(middleware, container);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
// Find route
|
|
44
54
|
const route = explorerData.routes.find(r => r.op.operationId === hash);
|
|
45
55
|
if (route) {
|
|
@@ -60,19 +70,202 @@ function renderInfoSection(info) {
|
|
|
60
70
|
`;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
function renderMiddlewareView(middleware, container) {
|
|
74
|
+
const html = `
|
|
75
|
+
<div class="middleware-detail-view">
|
|
76
|
+
<div class="middleware-header">
|
|
77
|
+
<h1>${middleware.name}</h1>
|
|
78
|
+
<div class="middleware-meta">
|
|
79
|
+
${middleware.scope ? `<span class="badge">${middleware.scope}</span>` : ''}
|
|
80
|
+
${middleware.file ? `
|
|
81
|
+
<a href="vscode://file/${middleware.file}:${middleware.startLine || 1}" class="doc-source-link">
|
|
82
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
83
|
+
<polyline points="16 18 22 12 16 6"></polyline>
|
|
84
|
+
<polyline points="8 6 2 12 8 18"></polyline>
|
|
85
|
+
</svg>
|
|
86
|
+
${middleware.file.split('/').pop()}:${middleware.startLine || 1}
|
|
87
|
+
</a>
|
|
88
|
+
` : ''}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
${middleware.responseTypes || middleware.headers ? `
|
|
93
|
+
<div class="middleware-capabilities">
|
|
94
|
+
${middleware.responseTypes ? `
|
|
95
|
+
<div class="capability-section">
|
|
96
|
+
<h3>Response Types</h3>
|
|
97
|
+
<div class="response-list">
|
|
98
|
+
${Object.entries(middleware.responseTypes).map(([code, resp]) => `
|
|
99
|
+
<div class="response-item">
|
|
100
|
+
<code class="status-code">${code}</code>
|
|
101
|
+
<span>${resp.description || 'Response'}</span>
|
|
102
|
+
</div>
|
|
103
|
+
`).join('')}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
` : ''}
|
|
107
|
+
${middleware.headers && middleware.headers.length > 0 ? `
|
|
108
|
+
<div class="capability-section">
|
|
109
|
+
<h3>Headers</h3>
|
|
110
|
+
<div class="header-list">
|
|
111
|
+
${middleware.headers.map(h => `<code class="header-name">${h}</code>`).join('')}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
` : ''}
|
|
115
|
+
</div>
|
|
116
|
+
` : ''}
|
|
117
|
+
|
|
118
|
+
${middleware.file ? `
|
|
119
|
+
<div class="source-section">
|
|
120
|
+
<h3>Source Code</h3>
|
|
121
|
+
<div id="monaco-middleware-source" class="monaco-container" style="height: 400px;"></div>
|
|
122
|
+
</div>
|
|
123
|
+
` : ''}
|
|
124
|
+
|
|
125
|
+
${middleware.usedBy && middleware.usedBy.length > 0 ? `
|
|
126
|
+
<div class="usage-section">
|
|
127
|
+
<h3>Used By (${middleware.usedBy.length} routes)</h3>
|
|
128
|
+
<div class="table-container">
|
|
129
|
+
<table class="data-table">
|
|
130
|
+
<thead>
|
|
131
|
+
<tr>
|
|
132
|
+
<th>Method</th>
|
|
133
|
+
<th>Path</th>
|
|
134
|
+
<th>Description</th>
|
|
135
|
+
</tr>
|
|
136
|
+
</thead>
|
|
137
|
+
<tbody>
|
|
138
|
+
${middleware.usedBy.map(routePath => {
|
|
139
|
+
const route = explorerData.routes.find(r => r.path === routePath);
|
|
140
|
+
if (route) {
|
|
141
|
+
return `
|
|
142
|
+
<tr>
|
|
143
|
+
<td class="col-method"><span class="badge badge-${route.method.toUpperCase()}">${route.method.toUpperCase()}</span></td>
|
|
144
|
+
<td class="col-path"><a href="#${route.op.operationId}" class="route-link">${routePath}</a></td>
|
|
145
|
+
<td class="col-desc">${route.op.summary || route.op.description || '-'}</td>
|
|
146
|
+
</tr>
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
return `
|
|
150
|
+
<tr>
|
|
151
|
+
<td colspan="3">${routePath} (Route definition not found)</td>
|
|
152
|
+
</tr>
|
|
153
|
+
`;
|
|
154
|
+
}).join('')}
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
` : ''}
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
container.innerHTML = html;
|
|
164
|
+
|
|
165
|
+
// Initialize Monaco for source code
|
|
166
|
+
if (middleware.file && typeof require !== 'undefined') {
|
|
167
|
+
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
|
|
168
|
+
require(['vs/editor/editor.main'], function () {
|
|
169
|
+
const monacoContainer = document.getElementById('monaco-middleware-source');
|
|
170
|
+
if (monacoContainer) {
|
|
171
|
+
if (currentEditors.source) currentEditors.source.dispose();
|
|
172
|
+
|
|
173
|
+
currentEditors.source = monaco.editor.create(monacoContainer, {
|
|
174
|
+
value: '// Loading source...',
|
|
175
|
+
language: 'typescript',
|
|
176
|
+
theme: 'vs-dark',
|
|
177
|
+
minimap: { enabled: false },
|
|
178
|
+
lineNumbers: 'on',
|
|
179
|
+
readOnly: true,
|
|
180
|
+
scrollBeyondLastLine: false,
|
|
181
|
+
automaticLayout: true,
|
|
182
|
+
glyphMargin: false,
|
|
183
|
+
folding: false,
|
|
184
|
+
lineNumbersMinChars: 3,
|
|
185
|
+
fontSize: 13,
|
|
186
|
+
fontFamily: 'JetBrains Mono',
|
|
187
|
+
renderLineHighlight: 'none'
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Load source
|
|
191
|
+
fetch(`_source?file=${encodeURIComponent(middleware.file)}`)
|
|
192
|
+
.then(res => {
|
|
193
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
194
|
+
return res.text();
|
|
195
|
+
})
|
|
196
|
+
.then(text => {
|
|
197
|
+
if (currentEditors.source) {
|
|
198
|
+
currentEditors.source.setValue(text);
|
|
199
|
+
|
|
200
|
+
// Highlight the middleware function
|
|
201
|
+
if (middleware.startLine) {
|
|
202
|
+
const endLine = middleware.endLine || middleware.startLine;
|
|
203
|
+
const decorations = [{
|
|
204
|
+
range: new monaco.Range(middleware.startLine, 1, endLine, 1),
|
|
205
|
+
options: {
|
|
206
|
+
isWholeLine: true,
|
|
207
|
+
className: 'closure-highlight'
|
|
208
|
+
}
|
|
209
|
+
}];
|
|
210
|
+
currentEditors.source.deltaDecorations([], decorations);
|
|
211
|
+
currentEditors.source.revealLineInCenter(middleware.startLine);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
.catch(err => {
|
|
216
|
+
if (currentEditors.source) {
|
|
217
|
+
currentEditors.source.setValue(`// Failed to load source: ${err.message}`);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
63
225
|
// Helper to recursively render schema properties
|
|
64
|
-
function renderSchema(schema, depth = 0) {
|
|
226
|
+
function renderSchema(schema, depth = 0, isResponse = false) {
|
|
65
227
|
if (!schema) return '';
|
|
66
228
|
|
|
67
229
|
const indent = depth * 16;
|
|
68
230
|
const type = schema.type || 'any';
|
|
69
231
|
const required = schema.required || [];
|
|
70
232
|
|
|
233
|
+
// Handle oneOf (multiple possible schemas)
|
|
234
|
+
if (schema.oneOf) {
|
|
235
|
+
return `
|
|
236
|
+
<div style="margin-left: ${indent}px;">
|
|
237
|
+
<div style="font-weight: 500; color: var(--text-primary); margin-bottom: 8px;">
|
|
238
|
+
<span style="color: var(--text-secondary); font-size: 0.85rem;">One of the following:</span>
|
|
239
|
+
</div>
|
|
240
|
+
${schema.oneOf.map((subSchema, idx) => `
|
|
241
|
+
<div style="border-left: 3px solid #4caf50; padding-left: 12px; margin-bottom: 12px;">
|
|
242
|
+
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 4px;">Option ${idx + 1}:</div>
|
|
243
|
+
${renderSchema(subSchema, 0, isResponse)}
|
|
244
|
+
</div>
|
|
245
|
+
`).join('')}
|
|
246
|
+
</div>
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
|
|
71
250
|
if (type === 'object' && schema.properties) {
|
|
72
251
|
const props = Object.entries(schema.properties).map(([key, prop]) => {
|
|
73
252
|
const isRequired = required.includes(key);
|
|
74
|
-
const
|
|
75
|
-
const
|
|
253
|
+
const isUnknown = prop['x-unknown'] === true;
|
|
254
|
+
const propType = isUnknown ? 'unknown' : (prop.type || 'any');
|
|
255
|
+
const hasNested = (prop.type === 'object' && prop.properties) || (prop.type === 'array' && prop.items);
|
|
256
|
+
|
|
257
|
+
// For responses, show "optional" for non-required fields
|
|
258
|
+
// For requests, show "required" for required fields
|
|
259
|
+
let badgeHtml = '';
|
|
260
|
+
if (isResponse) {
|
|
261
|
+
if (!isRequired) {
|
|
262
|
+
badgeHtml = '<div class="property-optional" style="margin-left: auto; font-size: 0.75rem; color: #9e9e9e; text-transform: uppercase; font-style: italic;">optional</div>';
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
if (isRequired) {
|
|
266
|
+
badgeHtml = '<div class="property-required" style="margin-left: auto; font-size: 0.75rem; color: #f44336; text-transform: uppercase;">required</div>';
|
|
267
|
+
}
|
|
268
|
+
}
|
|
76
269
|
|
|
77
270
|
return `
|
|
78
271
|
<div style="margin-left: ${indent}px;">
|
|
@@ -80,11 +273,12 @@ function renderSchema(schema, depth = 0) {
|
|
|
80
273
|
<div class="property-name" style="font-family: monospace; font-weight: 500; color: var(--text-primary);">${key}</div>
|
|
81
274
|
<span class="property-detail" style="color: var(--text-secondary); font-size: 0.85rem;">
|
|
82
275
|
<span class="property-detail-value">${propType}</span>
|
|
276
|
+
${isUnknown ? '<span class="unknown-marker" title="Type could not be determined statically" style="color: #ff9800; margin-left: 4px;">⚠️</span>' : ''}
|
|
83
277
|
</span>
|
|
84
|
-
${
|
|
278
|
+
${badgeHtml}
|
|
85
279
|
</div>
|
|
86
280
|
${prop.description ? `<div style="color: var(--text-secondary); font-size: 0.85rem; margin-left: 0; margin-top: -4px; margin-bottom: 4px;">${prop.description}</div>` : ''}
|
|
87
|
-
${hasNested ? renderSchema(propType === 'array' ? prop.items : prop, depth + 1) : ''}
|
|
281
|
+
${hasNested ? renderSchema(propType === 'array' ? prop.items : prop, depth + 1, isResponse) : ''}
|
|
88
282
|
</div>
|
|
89
283
|
`;
|
|
90
284
|
}).join('');
|
|
@@ -95,12 +289,18 @@ function renderSchema(schema, depth = 0) {
|
|
|
95
289
|
<div style="font-family: monospace; font-size: 0.85rem; color: var(--text-secondary);">
|
|
96
290
|
[array items]
|
|
97
291
|
</div>
|
|
98
|
-
${renderSchema(schema.items, depth + 1)}
|
|
292
|
+
${renderSchema(schema.items, depth + 1, isResponse)}
|
|
99
293
|
</div>
|
|
100
294
|
`;
|
|
101
295
|
}
|
|
102
296
|
|
|
103
|
-
return
|
|
297
|
+
return `
|
|
298
|
+
<div style="margin-left: ${indent}px; padding: 4px 0;">
|
|
299
|
+
<span class="property-detail-value" style="color: var(--text-secondary); font-family: monospace;">${type}</span>
|
|
300
|
+
${schema.format ? `<span style="color: var(--text-secondary); font-size: 0.85rem; margin-left: 6px;">(${schema.format})</span>` : ''}
|
|
301
|
+
${schema.description ? `<div style="color: var(--text-secondary); font-size: 0.85rem; margin-top: 4px;">${schema.description}</div>` : ''}
|
|
302
|
+
</div>
|
|
303
|
+
`;
|
|
104
304
|
}
|
|
105
305
|
|
|
106
306
|
// Helper to highlight path operators
|
|
@@ -168,11 +368,38 @@ function renderRequestView(route, container) {
|
|
|
168
368
|
<div class="info-header">
|
|
169
369
|
<h2 class="info-title">${summary}</h2>
|
|
170
370
|
<div class="info-meta">
|
|
371
|
+
${op['x-shokupan-builtin'] ? `
|
|
372
|
+
<div class="meta-row">
|
|
373
|
+
<span class="builtin-badge" title="This endpoint is provided by a built-in plugin">
|
|
374
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
375
|
+
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
|
|
376
|
+
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
|
|
377
|
+
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
|
|
378
|
+
</svg>
|
|
379
|
+
Built-in Plugin ${op['x-shokupan-plugin-name'] ? `(${op['x-shokupan-plugin-name']})` : ''}
|
|
380
|
+
</span>
|
|
381
|
+
</div>
|
|
382
|
+
` : ''}
|
|
171
383
|
${op.tags ? `<div class="meta-row"><strong>Tags:</strong> ${op.tags.map(t => `<span class="badge">${t}</span>`).join('')}</div>` : ''}
|
|
172
384
|
</div>
|
|
173
385
|
</div>
|
|
174
386
|
|
|
175
387
|
${op.description ? `<div class="markdown-content" style="margin:16px 0;">${parseMarkdown(op.description)}</div>` : ''}
|
|
388
|
+
|
|
389
|
+
${op['x-source-info']?.isRuntime ? `
|
|
390
|
+
<div class="alert alert-warning" style="margin: 16px 0; padding: 12px; background: rgba(255, 152, 0, 0.1); border-left: 3px solid #ff9800; border-radius: 4px;">
|
|
391
|
+
<strong style="color: #ff9800;">⚠️ Warning:</strong> This route's path could not be statically determined.
|
|
392
|
+
<div style="margin-top: 4px; font-size: 0.9em; opacity: 0.8;">
|
|
393
|
+
The path depends on runtime variables (e.g., process.env). Static analysis features like type inference may be limited.
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
` : ''}
|
|
397
|
+
|
|
398
|
+
${op['x-warning'] ? `
|
|
399
|
+
<div class="alert alert-warning" style="margin: 16px 0; padding: 12px; background: rgba(255, 152, 0, 0.1); border-left: 3px solid #ff9800; border-radius: 4px;">
|
|
400
|
+
<strong style="color: #ff9800;">⚠️ Warning:</strong> ${op['x-warning-reason'] || 'This operation could not be fully analyzed statically'}
|
|
401
|
+
</div>
|
|
402
|
+
` : ''}
|
|
176
403
|
|
|
177
404
|
${op.tags && op.tags.length > 0 ? `
|
|
178
405
|
<div class="hierarchy-section" style="margin:16px 0;">
|
|
@@ -259,7 +486,7 @@ function renderRequestView(route, container) {
|
|
|
259
486
|
` : ''}
|
|
260
487
|
${schema ? `
|
|
261
488
|
<div style="margin-top: 8px; background: var(--bg-primary); padding: 8px; border-radius: 4px;">
|
|
262
|
-
${renderSchema(schema)}
|
|
489
|
+
${renderSchema(schema, 0, true)}
|
|
263
490
|
</div>
|
|
264
491
|
` : ''}
|
|
265
492
|
</div>
|
|
@@ -272,13 +499,18 @@ function renderRequestView(route, container) {
|
|
|
272
499
|
${source ? `
|
|
273
500
|
<div class="source-section">
|
|
274
501
|
<h3 style="margin-bottom:8px; font-size:1.1rem; color:var(--text-primary);">Source Code</h3>
|
|
275
|
-
<div class="source-header" style="justify-content:
|
|
502
|
+
<div class="source-header" style="justify-content: space-between; margin-bottom: 8px; align-items: center;">
|
|
276
503
|
<a href="vscode://file/${source.file}:${source.line}" class="doc-source-link" title="${source.file}:${source.line}">
|
|
277
504
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px">
|
|
278
505
|
<polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline>
|
|
279
506
|
</svg>
|
|
280
507
|
${source.file.split('/').pop()}:${source.line}
|
|
281
508
|
</a>
|
|
509
|
+
<button class="btn icon-btn" id="btn-source-fullscreen" title="Toggle Fullscreen">
|
|
510
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
511
|
+
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
|
|
512
|
+
</svg>
|
|
513
|
+
</button>
|
|
282
514
|
</div>
|
|
283
515
|
<div id="monaco-source-viewer" class="monaco-container"></div>
|
|
284
516
|
</div>
|
|
@@ -421,6 +653,48 @@ function renderRequestView(route, container) {
|
|
|
421
653
|
// Setup initial remove buttons
|
|
422
654
|
container.querySelectorAll('.remove-header').forEach(setupRemoveHeaderBtn);
|
|
423
655
|
|
|
656
|
+
// Source viewer fullscreen toggle
|
|
657
|
+
const fullscreenBtn = document.getElementById('btn-source-fullscreen');
|
|
658
|
+
if (fullscreenBtn) {
|
|
659
|
+
const updateFullscreenIcon = (isFullscreen) => {
|
|
660
|
+
const svg = fullscreenBtn.querySelector('svg');
|
|
661
|
+
if (isFullscreen) {
|
|
662
|
+
// Exit fullscreen icon (minimize)
|
|
663
|
+
svg.innerHTML = '<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path>';
|
|
664
|
+
} else {
|
|
665
|
+
// Enter fullscreen icon (maximize)
|
|
666
|
+
svg.innerHTML = '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>';
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
fullscreenBtn.addEventListener('click', () => {
|
|
671
|
+
const sourceSection = container.querySelector('.source-section');
|
|
672
|
+
if (sourceSection) {
|
|
673
|
+
const isFullscreen = sourceSection.classList.toggle('fullscreen');
|
|
674
|
+
updateFullscreenIcon(isFullscreen);
|
|
675
|
+
|
|
676
|
+
// Update Monaco layout after transition
|
|
677
|
+
setTimeout(() => {
|
|
678
|
+
if (currentEditors.source) currentEditors.source.layout();
|
|
679
|
+
}, 300);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// ESC key to exit fullscreen
|
|
684
|
+
document.addEventListener('keydown', (e) => {
|
|
685
|
+
if (e.key === 'Escape') {
|
|
686
|
+
const sourceSection = container.querySelector('.source-section');
|
|
687
|
+
if (sourceSection && sourceSection.classList.contains('fullscreen')) {
|
|
688
|
+
sourceSection.classList.remove('fullscreen');
|
|
689
|
+
updateFullscreenIcon(false);
|
|
690
|
+
setTimeout(() => {
|
|
691
|
+
if (currentEditors.source) currentEditors.source.layout();
|
|
692
|
+
}, 300);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
424
698
|
// Populate Request Body if example exists
|
|
425
699
|
if (hasBody && currentEditors.request) {
|
|
426
700
|
currentEditors.request.setValue('{\n \n}');
|
|
@@ -429,6 +703,132 @@ function renderRequestView(route, container) {
|
|
|
429
703
|
setupPanelResizer(container);
|
|
430
704
|
}
|
|
431
705
|
|
|
706
|
+
const STORAGE_PREFIX = 'shokupan:explorer:';
|
|
707
|
+
|
|
708
|
+
function saveState(key, value) {
|
|
709
|
+
try {
|
|
710
|
+
localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value));
|
|
711
|
+
} catch (e) {
|
|
712
|
+
console.warn('Failed to save state', e);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function getState(key, defaultValue) {
|
|
717
|
+
try {
|
|
718
|
+
const item = localStorage.getItem(STORAGE_PREFIX + key);
|
|
719
|
+
return item ? JSON.parse(item) : defaultValue;
|
|
720
|
+
} catch (e) {
|
|
721
|
+
return defaultValue;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function setupSidebar() {
|
|
726
|
+
const sidebar = document.querySelector('.sidebar');
|
|
727
|
+
const content = document.querySelector('.content');
|
|
728
|
+
if (!sidebar) return;
|
|
729
|
+
|
|
730
|
+
// Restore state
|
|
731
|
+
const savedWidth = getState('sidebar_width', 300);
|
|
732
|
+
const isCollapsed = getState('sidebar_collapsed', false);
|
|
733
|
+
|
|
734
|
+
if (savedWidth) sidebar.style.width = `${savedWidth}px`;
|
|
735
|
+
if (isCollapsed) {
|
|
736
|
+
sidebar.classList.add('collapsed');
|
|
737
|
+
content.classList.add('no-sidebar');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 1. Toggle Sidebar Logic
|
|
741
|
+
const toggleBtn = document.querySelector('.toggle-sidebar');
|
|
742
|
+
const expandBtn = document.querySelector('.sidebar-collapse-trigger');
|
|
743
|
+
|
|
744
|
+
const toggleSidebar = (collapse) => {
|
|
745
|
+
if (collapse) {
|
|
746
|
+
sidebar.classList.add('collapsed');
|
|
747
|
+
content.classList.add('no-sidebar');
|
|
748
|
+
saveState('sidebar_collapsed', true);
|
|
749
|
+
} else {
|
|
750
|
+
sidebar.classList.remove('collapsed');
|
|
751
|
+
content.classList.remove('no-sidebar');
|
|
752
|
+
saveState('sidebar_collapsed', false);
|
|
753
|
+
}
|
|
754
|
+
// Trigger resize for editors
|
|
755
|
+
setTimeout(() => {
|
|
756
|
+
Object.values(currentEditors).forEach(editor => editor && editor.layout());
|
|
757
|
+
}, 300);
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
if (toggleBtn) {
|
|
761
|
+
toggleBtn.addEventListener('click', () => toggleSidebar(true));
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (expandBtn) {
|
|
765
|
+
expandBtn.addEventListener('click', () => toggleSidebar(false));
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// 2. Resize Logic
|
|
769
|
+
setupSidebarResizer(sidebar);
|
|
770
|
+
|
|
771
|
+
// 3. Collapsible Groups (top-level)
|
|
772
|
+
document.querySelectorAll('.nav-group-title').forEach(title => {
|
|
773
|
+
title.addEventListener('click', (e) => {
|
|
774
|
+
const group = e.currentTarget.parentElement;
|
|
775
|
+
group.classList.toggle('collapsed');
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// 4. Collapsible Subgroups (nested)
|
|
780
|
+
document.querySelectorAll('.nav-subgroup-title').forEach(title => {
|
|
781
|
+
title.addEventListener('click', (e) => {
|
|
782
|
+
e.stopPropagation(); // Prevent bubbling to parent group
|
|
783
|
+
const subgroup = e.currentTarget.parentElement;
|
|
784
|
+
subgroup.classList.toggle('collapsed');
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function setupSidebarResizer(sidebar) {
|
|
790
|
+
const resizer = sidebar.querySelector('.resize-handle');
|
|
791
|
+
if (!resizer) return;
|
|
792
|
+
|
|
793
|
+
let isResizing = false;
|
|
794
|
+
let startX, startWidth;
|
|
795
|
+
|
|
796
|
+
resizer.addEventListener('mousedown', (e) => {
|
|
797
|
+
isResizing = true;
|
|
798
|
+
startX = e.clientX;
|
|
799
|
+
startWidth = sidebar.getBoundingClientRect().width;
|
|
800
|
+
|
|
801
|
+
sidebar.classList.add('resizing'); // Disable transition
|
|
802
|
+
document.body.style.cursor = 'col-resize';
|
|
803
|
+
document.body.style.userSelect = 'none';
|
|
804
|
+
e.preventDefault();
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
document.addEventListener('mousemove', (e) => {
|
|
808
|
+
if (!isResizing) return;
|
|
809
|
+
|
|
810
|
+
const newWidth = startWidth + (e.clientX - startX);
|
|
811
|
+
// Clean constraints: min 150px, max 800px
|
|
812
|
+
const clamped = Math.max(150, Math.min(800, newWidth));
|
|
813
|
+
|
|
814
|
+
sidebar.style.width = `${clamped}px`;
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
document.addEventListener('mouseup', () => {
|
|
818
|
+
if (isResizing) {
|
|
819
|
+
isResizing = false;
|
|
820
|
+
sidebar.classList.remove('resizing');
|
|
821
|
+
document.body.style.cursor = '';
|
|
822
|
+
document.body.style.userSelect = '';
|
|
823
|
+
|
|
824
|
+
saveState('sidebar_width', sidebar.getBoundingClientRect().width);
|
|
825
|
+
|
|
826
|
+
// Layout editors
|
|
827
|
+
Object.values(currentEditors).forEach(editor => editor && editor.layout());
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
432
832
|
function setupPanelResizer(container) {
|
|
433
833
|
const resizer = container.querySelector('.panel-resizer');
|
|
434
834
|
const topPanel = container.querySelector('.request-panel');
|
|
@@ -436,6 +836,10 @@ function setupPanelResizer(container) {
|
|
|
436
836
|
|
|
437
837
|
if (!resizer || !topPanel || !bottomPanel) return;
|
|
438
838
|
|
|
839
|
+
// Restore state
|
|
840
|
+
const savedSplit = getState('panel_split', 50); // Default 50%
|
|
841
|
+
topPanel.style.flex = `0 0 ${savedSplit}%`;
|
|
842
|
+
|
|
439
843
|
let isResizing = false;
|
|
440
844
|
|
|
441
845
|
resizer.addEventListener('mousedown', (e) => {
|
|
@@ -470,6 +874,13 @@ function setupPanelResizer(container) {
|
|
|
470
874
|
document.body.style.cursor = '';
|
|
471
875
|
document.body.style.userSelect = '';
|
|
472
876
|
|
|
877
|
+
// Save state
|
|
878
|
+
const currentFlex = topPanel.style.flex;
|
|
879
|
+
const match = currentFlex.match(/([\d.]+)%/);
|
|
880
|
+
if (match) {
|
|
881
|
+
saveState('panel_split', parseFloat(match[1]));
|
|
882
|
+
}
|
|
883
|
+
|
|
473
884
|
// Trigger monaco layout as size changed
|
|
474
885
|
if (currentEditors.request) currentEditors.request.layout();
|
|
475
886
|
if (currentEditors.response) currentEditors.response.layout();
|
|
@@ -562,11 +973,12 @@ function initMonaco() {
|
|
|
562
973
|
const endLine = highlights[1] || startLine;
|
|
563
974
|
|
|
564
975
|
if (startLine > 0) {
|
|
976
|
+
const highlightClass = sourceInfo.isRuntime ? 'closure-highlight-dynamic' : 'closure-highlight';
|
|
565
977
|
decorations.push({
|
|
566
978
|
range: new monaco.Range(startLine, 1, endLine, 1),
|
|
567
979
|
options: {
|
|
568
980
|
isWholeLine: true,
|
|
569
|
-
className:
|
|
981
|
+
className: highlightClass
|
|
570
982
|
}
|
|
571
983
|
});
|
|
572
984
|
currentEditors.source.revealLineInCenter(startLine);
|
|
@@ -581,6 +993,7 @@ function initMonaco() {
|
|
|
581
993
|
if (h.type === 'emit') className = 'emit-highlight';
|
|
582
994
|
else if (h.type === 'return-success') className = 'success-line-highlight';
|
|
583
995
|
else if (h.type === 'return-warning') className = 'warning-line-highlight';
|
|
996
|
+
else if (h.type === 'dynamic-path') className = 'closure-highlight-dynamic';
|
|
584
997
|
// Fallback for older 'return' type if any mixed
|
|
585
998
|
else if (h.type === 'return') className = 'warning-line-highlight';
|
|
586
999
|
|
|
@@ -857,24 +1270,4 @@ function parseMarkdown(text) {
|
|
|
857
1270
|
return marked.parse(text, { renderer });
|
|
858
1271
|
}
|
|
859
1272
|
|
|
860
|
-
function setupSidebar() {
|
|
861
|
-
const sidebar = document.querySelector('.sidebar');
|
|
862
|
-
if (!sidebar) return;
|
|
863
|
-
|
|
864
|
-
// Collapsible Groups (top-level)
|
|
865
|
-
document.querySelectorAll('.nav-group-title').forEach(title => {
|
|
866
|
-
title.addEventListener('click', (e) => {
|
|
867
|
-
const group = e.currentTarget.parentElement;
|
|
868
|
-
group.classList.toggle('collapsed');
|
|
869
|
-
});
|
|
870
|
-
});
|
|
871
1273
|
|
|
872
|
-
// Collapsible Subgroups (nested)
|
|
873
|
-
document.querySelectorAll('.nav-subgroup-title').forEach(title => {
|
|
874
|
-
title.addEventListener('click', (e) => {
|
|
875
|
-
e.stopPropagation(); // Prevent bubbling to parent group
|
|
876
|
-
const subgroup = e.currentTarget.parentElement;
|
|
877
|
-
subgroup.classList.toggle('collapsed');
|
|
878
|
-
});
|
|
879
|
-
});
|
|
880
|
-
}
|