w-web-api 1.0.52 → 1.0.55

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 (78) hide show
  1. package/README.md +1 -1
  2. package/dist/css/app.cad4a1ca.css +1 -0
  3. package/dist/index.tmp +4 -2
  4. package/dist/js/app-legacy.7687b23b.js +1 -0
  5. package/dist/js/app.7687b23b.js +1 -0
  6. package/dist/js/chunk-vendors-legacy.4c27cf51.js +15 -0
  7. package/dist/js/chunk-vendors.4c27cf51.js +15 -0
  8. package/dist/w-web-api.umd.js +2 -2
  9. package/dist/w-web-api.umd.js.map +1 -1
  10. package/docs/WWebApi.html +2 -2
  11. package/docs/index.html +1 -1
  12. package/docs/{WWebApi.mjs.html → server_WWebApi.mjs.html} +113 -66
  13. package/docs/src_components_Common_CheckYesNo.vue.html +187 -0
  14. package/g.getSettings.mjs +3 -0
  15. package/logs/2026-06-19T16.log +6 -0
  16. package/logs/2026-06-20T11.log +4 -0
  17. package/logs/2026-06-20T14.log +2 -0
  18. package/logs/2026-06-21T20.log +1 -0
  19. package/logs/2026-06-21T21.log +4 -0
  20. package/logs/2026-06-22T07.log +12 -0
  21. package/logs/2026-06-22T08.log +808 -0
  22. package/logs/2026-06-22T09.log +104 -0
  23. package/logs/2026-06-22T10.log +26 -0
  24. package/logs/2026-06-22T11.log +860 -0
  25. package/logs/2026-06-22T12.log +430 -0
  26. package/logs/2026-06-22T23.log +1547 -0
  27. package/logs/2026-06-23T00.log +473 -0
  28. package/logs/2026-06-23T08.log +231 -0
  29. package/logs/2026-06-23T09.log +179 -0
  30. package/logs/2026-07-01T11.log +488 -0
  31. package/logs/2026-07-09T22.log +370 -0
  32. package/logs/2026-07-09T23.log +370 -0
  33. package/logs/2026-07-10T12.log +284 -0
  34. package/logs/2026-07-10T13.log +777 -0
  35. package/logs/2026-07-11T17.log +2 -0
  36. package/package.json +20 -15
  37. package/public/index.html +9 -2
  38. package/server/WWebApi.mjs +110 -63
  39. package/server/procApis.mjs +62 -0
  40. package/server/procLang.mjs +372 -66
  41. package/server/procProxy.mjs +120 -0
  42. package/server/srLog.mjs +30 -0
  43. package/server/staLogs/filterVpfsByWindow.mjs +27 -0
  44. package/server/staLogs/staEvent.mjs +131 -0
  45. package/server/staLogs/test_staEvent.mjs +198 -0
  46. package/settings.json +18 -0
  47. package/src/App.vue +11 -7
  48. package/src/assets/scalar-ui.css +113 -0
  49. package/src/components/Common/CheckYes.vue +102 -0
  50. package/src/components/Common/CheckYesNo.vue +115 -0
  51. package/src/components/Common/LoadingWinBar.vue +114 -0
  52. package/src/components/Layout.vue +8 -26
  53. package/src/components/LayoutContent.vue +299 -350
  54. package/src/components/LayoutContentEdit.vue +577 -0
  55. package/src/components/LayoutContentStats.vue +485 -0
  56. package/src/components/LayoutContentTest.vue +781 -0
  57. package/src/components/MdPanel.vue +80 -58
  58. package/src/main.js +11 -8
  59. package/src/plugins/mUI.mjs +19 -39
  60. package/src/schema/tables/apis.mjs +30 -0
  61. package/src/store/mutations.mjs +0 -14
  62. package/src/store/types.mjs +0 -3
  63. package/srv.mjs +50 -20
  64. package/tableTags.json +1 -1
  65. package/z/344/277/256/346/255/243/350/246/217/345/212/203/346/233/270.md +246 -0
  66. package/.github/workflows/ci-test.yml +0 -24
  67. package/.vscode/launch.json +0 -18
  68. package/db/wapis/apis/data.mdb +0 -0
  69. package/db/wapis/apis/lock.mdb +0 -0
  70. package/dist/css/app.b5cf2ef5.css +0 -1
  71. package/dist/js/app-legacy.927b5679.js +0 -1
  72. package/dist/js/app.927b5679.js +0 -1
  73. package/dist/js/chunk-vendors-legacy.ffb0c6c5.js +0 -13
  74. package/dist/js/chunk-vendors.ffb0c6c5.js +0 -13
  75. package/src/plugins/mDataSelectorSchema.mjs +0 -30
  76. package/src/plugins/mShare.mjs +0 -44
  77. package/tableTags-web-api.json +0 -1
  78. package/test/all.test.mjs +0 -10
@@ -0,0 +1,781 @@
1
+ <template>
2
+ <div :style="`height:${height}px; overflow-y:auto; padding:16px;`">
3
+
4
+ <!-- 頂部:method + url + 送出(address bar 群組) -->
5
+ <div class="addr-bar" style="margin-bottom:12px;">
6
+
7
+ <div class="addr-method">
8
+ <WTextSelect
9
+ :items="methodItems"
10
+ :value="req.method"
11
+ @input="onInputMethod"
12
+ >
13
+ <template v-slot:select="props">
14
+ <span :style="`color:${getMethodColor(props.item)};`">{{props.item}}</span>
15
+ </template>
16
+ <template v-slot:item="props">
17
+ <span :style="`color:${getMethodColor(props.item)};`">{{props.item}}</span>
18
+ </template>
19
+ </WTextSelect>
20
+ </div>
21
+
22
+ <input
23
+ v-model="req.url"
24
+ type="text"
25
+ class="addr-url"
26
+ :placeholder="$t('reqUrlPlaceholder')"
27
+ />
28
+
29
+ <button
30
+ @click="onClickSend"
31
+ :disabled="sending"
32
+ class="addr-send"
33
+ :style="sending ? 'opacity:0.6; cursor:not-allowed;' : ''"
34
+ >
35
+ {{$t('reqSend')}}
36
+ </button>
37
+
38
+ </div>
39
+
40
+ <!-- 錯誤訊息 -->
41
+ <div
42
+ v-if="errSend"
43
+ class="err-bar"
44
+ style="margin-bottom:10px;"
45
+ >
46
+ {{errSend}}
47
+ </div>
48
+
49
+ <!-- Query 參數 -->
50
+ <div style="margin-bottom:12px;">
51
+
52
+ <div class="sec-label" style="margin-bottom:6px;">
53
+ {{$t('reqQuery')}}
54
+ </div>
55
+
56
+ <div class="kv-table">
57
+
58
+ <div class="kv-head">
59
+ <div class="kv-col-ck">{{$t('colOn')}}</div>
60
+ <div class="kv-col-key">{{$t('colKey')}}</div>
61
+ <div class="kv-col-val">{{$t('colValue')}}</div>
62
+ <div class="kv-col-del"></div>
63
+ </div>
64
+
65
+ <div
66
+ v-for="(row, idx) in req.query"
67
+ :key="idx"
68
+ class="kv-row"
69
+ >
70
+ <div class="kv-col-ck kv-center">
71
+ <input type="checkbox" v-model="row.on" style="cursor:pointer;" />
72
+ </div>
73
+ <div class="kv-col-key">
74
+ <input
75
+ v-model="row.key"
76
+ type="text"
77
+ class="kv-input"
78
+ />
79
+ </div>
80
+ <div class="kv-col-val">
81
+ <input
82
+ v-model="row.value"
83
+ type="text"
84
+ class="kv-input"
85
+ />
86
+ </div>
87
+ <div class="kv-col-del kv-center">
88
+ <button
89
+ @click="(/* */) => removeQueryRow(idx)"
90
+ class="kv-del-btn"
91
+ >×</button>
92
+ </div>
93
+ </div>
94
+
95
+ </div>
96
+
97
+ <div style="margin-top:8px;">
98
+ <button
99
+ @click="addQueryRow"
100
+ class="btn-addrow"
101
+ >
102
+ + {{$t('reqAddRow')}}
103
+ </button>
104
+ </div>
105
+
106
+ </div>
107
+
108
+ <!-- Headers -->
109
+ <div style="margin-bottom:12px;">
110
+
111
+ <div class="sec-label" style="margin-bottom:6px;">
112
+ {{$t('reqHeaders')}}
113
+ </div>
114
+
115
+ <div class="kv-table">
116
+
117
+ <div class="kv-head">
118
+ <div class="kv-col-ck">{{$t('colOn')}}</div>
119
+ <div class="kv-col-key">{{$t('colKey')}}</div>
120
+ <div class="kv-col-val">{{$t('colValue')}}</div>
121
+ <div class="kv-col-del"></div>
122
+ </div>
123
+
124
+ <div
125
+ v-for="(row, idx) in req.headers"
126
+ :key="idx"
127
+ class="kv-row"
128
+ >
129
+ <div class="kv-col-ck kv-center">
130
+ <input type="checkbox" v-model="row.on" style="cursor:pointer;" />
131
+ </div>
132
+ <div class="kv-col-key">
133
+ <input
134
+ v-model="row.key"
135
+ type="text"
136
+ class="kv-input"
137
+ />
138
+ </div>
139
+ <div class="kv-col-val">
140
+ <input
141
+ v-model="row.value"
142
+ type="text"
143
+ class="kv-input"
144
+ />
145
+ </div>
146
+ <div class="kv-col-del kv-center">
147
+ <button
148
+ @click="(/* */) => removeHeaderRow(idx)"
149
+ class="kv-del-btn"
150
+ >×</button>
151
+ </div>
152
+ </div>
153
+
154
+ </div>
155
+
156
+ <div style="margin-top:8px;">
157
+ <button
158
+ @click="addHeaderRow"
159
+ class="btn-addrow"
160
+ >
161
+ + {{$t('reqAddRow')}}
162
+ </button>
163
+ </div>
164
+
165
+ </div>
166
+
167
+ <!-- Body(僅 non-GET/HEAD 顯示) -->
168
+ <div
169
+ v-if="showBody"
170
+ style="margin-bottom:12px;"
171
+ >
172
+
173
+ <div class="sec-label" style="margin-bottom:6px;">
174
+ {{$t('reqBody')}}
175
+ </div>
176
+
177
+ <textarea
178
+ v-model="req.body"
179
+ class="body-textarea"
180
+ ></textarea>
181
+
182
+ </div>
183
+
184
+ <!-- 回應區 -->
185
+ <div>
186
+
187
+ <div class="sec-label" style="margin-bottom:8px;">
188
+ {{$t('resTitle')}}
189
+ </div>
190
+
191
+ <!-- 空狀態 -->
192
+ <div
193
+ v-if="res === null"
194
+ class="card res-empty-card"
195
+ >
196
+ <div class="card-h" style="border-bottom:none; background:transparent;">
197
+ <span style="color:var(--c-3); font-size:var(--fs-sm);">{{$t('resEmpty')}}</span>
198
+ </div>
199
+ </div>
200
+
201
+ <!-- 有回應 -->
202
+ <div v-else class="card">
203
+
204
+ <!-- status + time -->
205
+ <div class="card-h">
206
+ <span class="w-status" :class="statusClass">{{res.status}} {{res.statusText}}</span>
207
+ <span class="w-muted w-tnum" style="font-size:12px; margin-left:auto;">{{$t('resTime')}} {{res.durationMs}} ms</span>
208
+ </div>
209
+
210
+ <!-- response headers -->
211
+ <div class="card-h"><span class="lab" style="font-size:11.5px;">{{$t('resHeaders')}}</span></div>
212
+ <pre class="code" style="white-space:pre-wrap; word-break:break-all;">{{resHeadersStr}}</pre>
213
+
214
+ <!-- response body -->
215
+ <div class="card-h"><span class="lab" style="font-size:11.5px;">{{$t('resBody')}}</span></div>
216
+ <pre class="code" style="white-space:pre-wrap; word-break:break-all;" v-html="bodyHighlighted"></pre>
217
+
218
+ </div>
219
+
220
+ </div>
221
+
222
+ </div>
223
+ </template>
224
+
225
+ <script>
226
+ import get from 'lodash-es/get.js'
227
+ import each from 'lodash-es/each.js'
228
+ import isestr from 'wsemi/src/isestr.mjs'
229
+ import isobj from 'wsemi/src/isobj.mjs'
230
+ import j2o from 'wsemi/src/j2o.mjs'
231
+ import WTextSelect from 'w-component-vue/src/components/WTextSelect.vue'
232
+
233
+
234
+ let kpColorMethod = {
235
+ GET: 'var(--m-get)',
236
+ POST: 'var(--m-post)',
237
+ PUT: 'var(--m-put)',
238
+ DEL: 'var(--m-del)',
239
+ }
240
+
241
+
242
+ export default {
243
+ components: {
244
+ WTextSelect,
245
+ },
246
+ props: {
247
+ item: {
248
+ type: Object,
249
+ default: function() { return {} },
250
+ },
251
+ height: {
252
+ type: Number,
253
+ default: 0,
254
+ },
255
+ },
256
+ data: function() {
257
+ return {
258
+ methodItems: ['GET', 'POST', 'PUT', 'DEL'],
259
+ req: {
260
+ method: 'GET',
261
+ url: '',
262
+ query: [{ on: true, key: '', value: '' }],
263
+ headers: [{ on: true, key: '', value: '' }],
264
+ body: '',
265
+ },
266
+ res: null,
267
+ sending: false,
268
+ errSend: '',
269
+ }
270
+ },
271
+ mounted: function() {
272
+ let vo = this
273
+ vo.seedFromItem(vo.item)
274
+ },
275
+ watch: {
276
+ item: {
277
+ immediate: false,
278
+ handler: function(val) {
279
+ let vo = this
280
+ vo.seedFromItem(val)
281
+ },
282
+ },
283
+ },
284
+ computed: {
285
+
286
+ showBody: function() {
287
+ let vo = this
288
+ let m = vo.req.method
289
+ return m !== 'GET' && m !== 'HEAD'
290
+ },
291
+
292
+ resHeadersStr: function() {
293
+ let vo = this
294
+ if (vo.res === null) {
295
+ return ''
296
+ }
297
+ try {
298
+ return JSON.stringify(vo.res.headers, null, 2)
299
+ }
300
+ catch (e) {
301
+ return String(vo.res.headers)
302
+ }
303
+ },
304
+
305
+ statusClass: function() {
306
+ let vo = this
307
+ if (vo.res === null) {
308
+ return ''
309
+ }
310
+ let s = parseInt(vo.res.status, 10)
311
+ if (s >= 200 && s < 300) return 's2'
312
+ if (s >= 300 && s < 400) return 's3'
313
+ if (s >= 400 && s < 500) return 's4'
314
+ if (s >= 500) return 's5'
315
+ return ''
316
+ },
317
+
318
+ bodyHighlighted: function() {
319
+ let vo = this
320
+ if (vo.res === null) {
321
+ return ''
322
+ }
323
+ // 取得原始字串
324
+ let raw = ''
325
+ if (isobj(vo.res.data)) {
326
+ try {
327
+ raw = JSON.stringify(vo.res.data, null, 2)
328
+ }
329
+ catch (e) {
330
+ raw = String(vo.res.data)
331
+ }
332
+ }
333
+ else {
334
+ raw = String(vo.res.data)
335
+ }
336
+ // HTML escape
337
+ let escaped = raw
338
+ .replace(/&/g, '&amp;')
339
+ .replace(/</g, '&lt;')
340
+ .replace(/>/g, '&gt;')
341
+ // 簡易 JSON 語法高亮(逐 token 替換)
342
+ // key: "xxx":
343
+ // string value: "xxx"
344
+ // number: bare number
345
+ // bool/null: true/false/null
346
+ let highlighted = escaped.replace(
347
+ /(&quot;[^&]*&quot;)\s*(:)|(&quot;[^&]*&quot;)|(\b-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(\b(?:true|false|null)\b)/g,
348
+ function(match, keyStr, colon, strVal, numVal, boolVal) {
349
+ if (keyStr && colon) {
350
+ return '<span class="k">' + keyStr + '</span>' + colon
351
+ }
352
+ if (strVal) {
353
+ return '<span class="s">' + strVal + '</span>'
354
+ }
355
+ if (numVal !== undefined) {
356
+ return '<span class="n">' + numVal + '</span>'
357
+ }
358
+ if (boolVal !== undefined) {
359
+ return '<span class="b">' + boolVal + '</span>'
360
+ }
361
+ return match
362
+ }
363
+ )
364
+ return highlighted
365
+ },
366
+
367
+ },
368
+ methods: {
369
+
370
+ getMethodColor: function(method) {
371
+ // let vo = this
372
+ return get(kpColorMethod, method, 'var(--c-2)')
373
+ },
374
+
375
+ jsonToRows: function(jsonStr) {
376
+ // let vo = this
377
+ let rows = []
378
+ if (!isestr(jsonStr)) {
379
+ return rows
380
+ }
381
+ let obj = j2o(jsonStr)
382
+ if (!isobj(obj)) {
383
+ return rows
384
+ }
385
+ each(obj, function(v, k) {
386
+ rows.push({ on: true, key: k, value: String(v) })
387
+ })
388
+ return rows
389
+ },
390
+
391
+ seedFromItem: function(itm) {
392
+ let vo = this
393
+
394
+ if (!itm) {
395
+ return
396
+ }
397
+
398
+ // method
399
+ let method = isestr(itm.method) ? itm.method.toUpperCase() : 'GET'
400
+ vo.req.method = method
401
+
402
+ // url
403
+ vo.req.url = isestr(itm.url) ? itm.url : (isestr(itm.testBaseUrl) ? itm.testBaseUrl : '')
404
+
405
+ // headers / query 預設(先建好, 認證可往兩者塞)
406
+ let headerRows = vo.jsonToRows(itm.defaultHeadersJson)
407
+ let queryRows = vo.jsonToRows(itm.defaultQueryJson)
408
+
409
+ // 認證:依 authType 解析 authConfigJson 自動帶入(none / bearer / apikey / basic)
410
+ let authType = isestr(itm.authType) ? itm.authType : 'none'
411
+ let authCfg = j2o(itm.authConfigJson)
412
+ if (!isobj(authCfg)) {
413
+ authCfg = {}
414
+ }
415
+ if (authType === 'bearer') {
416
+ // { "token": "xxx" };留空則退用 tokens 欄第一個(向後相容舊資料)
417
+ let token = isestr(authCfg.token) ? authCfg.token : (isestr(itm.tokens) ? itm.tokens.split(';')[0] : '')
418
+ if (isestr(token)) {
419
+ headerRows.push({ on: true, key: 'Authorization', value: 'Bearer ' + token })
420
+ }
421
+ }
422
+ else if (authType === 'apikey') {
423
+ // { "name": "X-API-Key", "value": "xxx", "in": "header"|"query" }
424
+ let name = isestr(authCfg.name) ? authCfg.name : 'X-API-Key'
425
+ let value = isestr(authCfg.value) ? authCfg.value : ''
426
+ if (isestr(value)) {
427
+ if (authCfg.in === 'query') {
428
+ queryRows.push({ on: true, key: name, value: value })
429
+ }
430
+ else {
431
+ headerRows.push({ on: true, key: name, value: value })
432
+ }
433
+ }
434
+ }
435
+ else if (authType === 'basic') {
436
+ // { "username": "u", "password": "p" } → Authorization: Basic base64(u:p)
437
+ let u = isestr(authCfg.username) ? authCfg.username : ''
438
+ let p = isestr(authCfg.password) ? authCfg.password : ''
439
+ if (isestr(u) || isestr(p)) {
440
+ headerRows.push({ on: true, key: 'Authorization', value: 'Basic ' + btoa(u + ':' + p) })
441
+ }
442
+ }
443
+
444
+ // content-type 標頭
445
+ if (isestr(itm.contentType)) {
446
+ headerRows.push({ on: true, key: 'Content-Type', value: itm.contentType })
447
+ }
448
+
449
+ if (headerRows.length === 0) {
450
+ headerRows.push({ on: true, key: '', value: '' })
451
+ }
452
+ vo.req.headers = headerRows
453
+
454
+ if (queryRows.length === 0) {
455
+ queryRows.push({ on: true, key: '', value: '' })
456
+ }
457
+ vo.req.query = queryRows
458
+
459
+ // body
460
+ let body = ''
461
+ if (isestr(itm.defaultBodyJson)) {
462
+ body = itm.defaultBodyJson
463
+ }
464
+ else if (isestr(itm.inputExample)) {
465
+ body = itm.inputExample
466
+ }
467
+ vo.req.body = body
468
+
469
+ // reset response
470
+ vo.res = null
471
+ vo.errSend = ''
472
+
473
+ },
474
+
475
+ addQueryRow: function() {
476
+ let vo = this
477
+ vo.req.query.push({ on: true, key: '', value: '' })
478
+ },
479
+
480
+ removeQueryRow: function(idx) {
481
+ let vo = this
482
+ vo.req.query.splice(idx, 1)
483
+ if (vo.req.query.length === 0) {
484
+ vo.req.query.push({ on: true, key: '', value: '' })
485
+ }
486
+ },
487
+
488
+ addHeaderRow: function() {
489
+ let vo = this
490
+ vo.req.headers.push({ on: true, key: '', value: '' })
491
+ },
492
+
493
+ removeHeaderRow: function(idx) {
494
+ let vo = this
495
+ vo.req.headers.splice(idx, 1)
496
+ if (vo.req.headers.length === 0) {
497
+ vo.req.headers.push({ on: true, key: '', value: '' })
498
+ }
499
+ },
500
+
501
+ onInputMethod: function(val) {
502
+ let vo = this
503
+ vo.req.method = val
504
+ },
505
+
506
+ onClickSend: function() {
507
+ let vo = this
508
+ vo.submitSend()
509
+ },
510
+
511
+ submitSend: function() {
512
+ let vo = this
513
+
514
+ let core = async function() {
515
+
516
+ // 1) 清空舊錯誤與結果
517
+ vo.errSend = ''
518
+ vo.res = null
519
+
520
+ // 2) 同步檢測
521
+ if (!isestr(vo.req.url)) {
522
+ vo.errSend = vo.$t('valRequired')
523
+ return
524
+ }
525
+
526
+ // 3) 開啟本地 loading
527
+ vo.sending = true
528
+
529
+ // 4) 組 spec 並送出
530
+ let method = vo.req.method
531
+
532
+ let query = {}
533
+ each(vo.req.query, function(row) {
534
+ if (row.on && isestr(row.key)) {
535
+ query[row.key] = row.value
536
+ }
537
+ })
538
+
539
+ let headers = {}
540
+ each(vo.req.headers, function(row) {
541
+ if (row.on && isestr(row.key)) {
542
+ headers[row.key] = row.value
543
+ }
544
+ })
545
+
546
+ let body = undefined
547
+ if (method !== 'GET' && method !== 'HEAD') {
548
+ let rawBody = vo.req.body
549
+ let ct = get(vo.item, 'contentType', '')
550
+ if (isestr(ct) && ct.toLowerCase().indexOf('json') >= 0) {
551
+ let parsed = j2o(rawBody)
552
+ body = isobj(parsed) ? parsed : rawBody
553
+ }
554
+ else {
555
+ body = rawBody
556
+ }
557
+ }
558
+
559
+ let spec = {
560
+ method: method,
561
+ url: vo.req.url,
562
+ headers: headers,
563
+ query: query,
564
+ body: body,
565
+ timeout: 30000,
566
+ }
567
+
568
+ let okSend = false
569
+ await vo.$fapi.proxyRequest(spec)
570
+ .then(function(r) {
571
+ vo.res = r
572
+ okSend = true
573
+ })
574
+ .catch(function(err) {
575
+ console.log('proxyRequest', err)
576
+ vo.errSend = vo.$transErr(err) //err 為後端 err-key,依 lang 反查顯示
577
+ })
578
+ if (!okSend) {
579
+ return
580
+ }
581
+
582
+ // 5) 全成功
583
+ return 'ok'
584
+
585
+ }
586
+
587
+ core()
588
+ .catch(function(err) {
589
+ console.log('catch', err)
590
+ vo.$alert(vo.$t('anUnexpectedErrorOccurred'), { type: 'error' })
591
+ })
592
+ .finally(function() {
593
+ vo.sending = false
594
+ })
595
+
596
+ },
597
+
598
+ },
599
+ }
600
+ </script>
601
+
602
+ <style scoped>
603
+
604
+ /* ---------- section label ---------- */
605
+ .sec-label {
606
+ font-size: var(--fs-xs);
607
+ font-weight: var(--fw-bold);
608
+ letter-spacing: .05em;
609
+ text-transform: uppercase;
610
+ color: var(--c-3);
611
+ }
612
+
613
+ /* ---------- address bar ---------- */
614
+ .addr-bar {
615
+ display: flex;
616
+ align-items: stretch;
617
+ border: 1px solid var(--border);
618
+ border-radius: var(--radius-lg);
619
+ overflow: hidden;
620
+ background: var(--bg-1);
621
+ }
622
+ .addr-method {
623
+ flex-shrink: 0;
624
+ display: flex;
625
+ align-items: center;
626
+ padding: 0 4px;
627
+ border-right: 1px solid var(--border);
628
+ background: var(--bg-2);
629
+ font-family: var(--font-code);
630
+ font-weight: var(--fw-bold);
631
+ font-size: var(--fs-sm);
632
+ }
633
+ .addr-url {
634
+ flex: 1;
635
+ border: none;
636
+ outline: none;
637
+ padding: 0 14px;
638
+ font-size: var(--fs-sm);
639
+ color: var(--c-1);
640
+ background: transparent;
641
+ font-family: var(--font-code);
642
+ }
643
+ .addr-send {
644
+ flex-shrink: 0;
645
+ margin: 5px;
646
+ padding: 0 20px;
647
+ border: none;
648
+ border-radius: var(--radius);
649
+ background: var(--accent);
650
+ color: #fff;
651
+ font-size: var(--fs-sm);
652
+ font-weight: var(--fw-medium);
653
+ cursor: pointer;
654
+ transition: background-color .15s;
655
+ }
656
+ .addr-send:hover:not(:disabled) {
657
+ background: var(--accent-hover);
658
+ }
659
+
660
+ /* ---------- error bar ---------- */
661
+ .err-bar {
662
+ padding: 8px 12px;
663
+ border-radius: var(--radius-lg);
664
+ background: var(--danger-bg);
665
+ border: 1px solid var(--danger);
666
+ color: var(--danger);
667
+ font-size: var(--fs-sm);
668
+ }
669
+
670
+ /* ---------- kv table (query / headers) ---------- */
671
+ .kv-table {
672
+ border: 1px solid var(--border);
673
+ border-radius: var(--radius-lg);
674
+ overflow: hidden;
675
+ }
676
+ .kv-head {
677
+ display: grid;
678
+ grid-template-columns: 52px 1fr 1fr 34px;
679
+ align-items: center;
680
+ background: var(--bg-2);
681
+ font-size: var(--fs-xs);
682
+ color: var(--c-2);
683
+ font-weight: var(--fw-bold);
684
+ white-space: nowrap;
685
+ }
686
+ .kv-head > div,
687
+ .kv-row > div {
688
+ padding: 7px 10px;
689
+ }
690
+ .kv-row {
691
+ display: grid;
692
+ grid-template-columns: 52px 1fr 1fr 34px;
693
+ align-items: center;
694
+ border-top: 1px solid var(--border);
695
+ }
696
+ .kv-center {
697
+ display: flex;
698
+ justify-content: center;
699
+ align-items: center;
700
+ }
701
+ .kv-col-ck { /* width handled by grid */ }
702
+ .kv-col-key { }
703
+ .kv-col-val { }
704
+ .kv-col-del { }
705
+
706
+ .kv-input {
707
+ width: 100%;
708
+ box-sizing: border-box;
709
+ border: 1px solid transparent;
710
+ background: transparent;
711
+ outline: none;
712
+ color: var(--c-1);
713
+ font-size: var(--fs-sm);
714
+ padding: 3px 6px;
715
+ border-radius: var(--radius-sm);
716
+ font-family: var(--font-code);
717
+ transition: border-color .15s, background-color .15s;
718
+ }
719
+ .kv-input:focus {
720
+ border-color: var(--accent);
721
+ background: var(--bg-1);
722
+ }
723
+
724
+ .kv-del-btn {
725
+ width: 22px;
726
+ height: 22px;
727
+ padding: 0;
728
+ border: none;
729
+ background: transparent;
730
+ color: var(--c-3);
731
+ font-size: 1rem;
732
+ cursor: pointer;
733
+ line-height: 1;
734
+ transition: color .15s;
735
+ }
736
+ .kv-del-btn:hover {
737
+ color: var(--danger);
738
+ }
739
+
740
+ /* ---------- add row button ---------- */
741
+ .btn-addrow {
742
+ padding: 5px 12px;
743
+ border: 1px solid var(--border);
744
+ border-radius: var(--radius-lg);
745
+ background: transparent;
746
+ color: var(--c-2);
747
+ font-size: var(--fs-xs);
748
+ cursor: pointer;
749
+ transition: background-color .15s;
750
+ }
751
+ .btn-addrow:hover {
752
+ background: var(--bg-3);
753
+ }
754
+
755
+ /* ---------- body textarea ---------- */
756
+ .body-textarea {
757
+ width: 100%;
758
+ min-height: 120px;
759
+ padding: 8px 10px;
760
+ border: 1px solid var(--border);
761
+ border-radius: var(--radius-lg);
762
+ font-size: var(--fs-sm);
763
+ font-family: var(--font-code);
764
+ color: var(--c-1);
765
+ background: var(--bg-1);
766
+ resize: vertical;
767
+ outline: none;
768
+ box-sizing: border-box;
769
+ transition: border-color .15s, box-shadow .15s;
770
+ }
771
+ .body-textarea:focus {
772
+ border-color: var(--accent);
773
+ box-shadow: var(--focus);
774
+ }
775
+
776
+ /* ---------- response panel ---------- */
777
+ .res-empty-card {
778
+ background: var(--bg-1);
779
+ }
780
+
781
+ </style>