@fish-under-sea/magpie-neartoken 0.1.8 → 0.2.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/README.md +10 -0
- package/index.mjs +21 -4
- package/package.json +1 -1
- package/page/app.js +188 -1
- package/page/style.css +66 -1
package/README.md
CHANGED
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
|
|
42
42
|
模型目录读的是站点的 `/api/pricing`(公开、全量),不用 `/v1/models`——那个端点按令牌分组过滤,很多账号拿到的是空列表,magpie 的「重新获取模型」就会报 `no models listed`。这里不碰它。
|
|
43
43
|
|
|
44
|
+
## 三个视角
|
|
45
|
+
|
|
46
|
+
页面上方三个标签:
|
|
47
|
+
|
|
48
|
+
| 标签 | 内容 |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| 原本界面 | 账户、余额与代金券、消耗看板、分组开关、充值、供应商说明 |
|
|
51
|
+
| 模型广场 | 按站点返回的分组重绘:低价 / 满血 / 图片 / 视频 / 语音 / 向量,每组一枚可点的开关(关掉即从 magpie 的模型列表里移除) |
|
|
52
|
+
| 使用日志 | 这个窗口里每个模型花了多少、用了多少 token、占比多少,可按日 / 周 / 月切 |
|
|
53
|
+
|
|
44
54
|
## 页面怎么用
|
|
45
55
|
|
|
46
56
|
1. 装上包,侧边栏「插件」→「NearToken」。
|
package/index.mjs
CHANGED
|
@@ -117,7 +117,7 @@ const hasWord = (id, words) => words.some((w) => id.toLowerCase().includes(w))
|
|
|
117
117
|
* 「站点原文组名 + 倍率 + 模型」。每行带自己的 x-neartoken-group 头,请求
|
|
118
118
|
* 按那一行走那一组——这就是"模型后面跟着它的分组"。
|
|
119
119
|
*/
|
|
120
|
-
async function catalog() {
|
|
120
|
+
async function catalog(disabled = new Set()) {
|
|
121
121
|
const env = await call('/api/pricing')
|
|
122
122
|
const rows = Array.isArray(env?.data) ? env.data : []
|
|
123
123
|
const refs = env?.group_refs && typeof env.group_refs === 'object' ? env.group_refs : {}
|
|
@@ -131,7 +131,9 @@ async function catalog() {
|
|
|
131
131
|
const drawer = hasWord(model, DRAWS) || ends.includes('image-generation')
|
|
132
132
|
const film = hasWord(model, FILMS)
|
|
133
133
|
if (!chat && !drawer && !film) continue
|
|
134
|
-
const
|
|
134
|
+
const all = Array.isArray(row?.enable_groups) && row.enable_groups.length ? row.enable_groups : ['']
|
|
135
|
+
const groups = all.filter((g) => !disabled.has(String(g ?? '').trim()))
|
|
136
|
+
if (!groups.length) continue
|
|
135
137
|
for (const raw of groups) {
|
|
136
138
|
const group = String(raw ?? '').trim()
|
|
137
139
|
const ref = group && refs[group] ? String(refs[group]) : ''
|
|
@@ -157,7 +159,22 @@ async function catalog() {
|
|
|
157
159
|
return out
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
export default async function neartoken() {
|
|
162
|
+
export default async function neartoken(input, options) {
|
|
163
|
+
// 页面写进来的开关:关掉的分组,它们的模型不再出现在 magpie 的列表里。
|
|
164
|
+
// 走的是 magpie 的 plugin options(页面 magpie.options.set),不碰宿主代码。
|
|
165
|
+
const disabled = (() => {
|
|
166
|
+
const raw = options?.disabledGroups
|
|
167
|
+
if (Array.isArray(raw)) return new Set(raw.filter((g) => typeof g === 'string'))
|
|
168
|
+
if (typeof raw === 'string' && raw.trim()) {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(raw)
|
|
171
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((g) => typeof g === 'string') : [])
|
|
172
|
+
} catch {
|
|
173
|
+
return new Set()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return new Set()
|
|
177
|
+
})()
|
|
161
178
|
return {
|
|
162
179
|
auth: {
|
|
163
180
|
provider: PROVIDER_ID,
|
|
@@ -274,7 +291,7 @@ export default async function neartoken() {
|
|
|
274
291
|
* @returns {Promise<Record<string, object>>}
|
|
275
292
|
*/
|
|
276
293
|
async models() {
|
|
277
|
-
return catalog()
|
|
294
|
+
return catalog(disabled)
|
|
278
295
|
},
|
|
279
296
|
},
|
|
280
297
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fish-under-sea/magpie-neartoken",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "NearToken for magpie: a provider whose sign-in is the site's own device authorization, models listed per billing group with their multiplier, balance and vouchers on magpie's usage card, plus a page for spending and top-ups.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
package/page/app.js
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
const windows = new Map()
|
|
19
19
|
/** 充值面:档位、渠道、当前选择。 */
|
|
20
20
|
let topupInfo = null
|
|
21
|
+
/** 插件自己的开关:关掉的分组(写进 magpie 的 plugin options,provider 插件按它过滤)。 */
|
|
22
|
+
let options = {}
|
|
23
|
+
let disabledGroups = new Set()
|
|
24
|
+
/** 当前视角:home / market / logs。 */
|
|
25
|
+
let tab = 'home'
|
|
21
26
|
const pick = { amount: null, method: null, custom: '' }
|
|
22
27
|
/** 正在等宿主的哪个动作。 */
|
|
23
28
|
let pendingAct = ''
|
|
@@ -92,12 +97,22 @@
|
|
|
92
97
|
// ---------- 主流程 ----------
|
|
93
98
|
|
|
94
99
|
async function boot() {
|
|
100
|
+
// 预览用:?tab=market 可直接落在某个视角,生产里没这个参数就是原本界面
|
|
101
|
+
const want = new URLSearchParams(location.search).get('tab')
|
|
102
|
+
if (want === 'market' || want === 'logs' || want === 'home') tab = want
|
|
95
103
|
if (!bridge || typeof bridge.call !== 'function') {
|
|
96
104
|
fail('这个 magpie 还没有插件页的调用通道。')
|
|
97
105
|
return
|
|
98
106
|
}
|
|
99
107
|
try {
|
|
100
108
|
await bridge.ready
|
|
109
|
+
// 插件自己的开关(关掉的分组):写在这里,provider 插件按它过滤模型
|
|
110
|
+
try {
|
|
111
|
+
options = (await bridge.options.get()) || {}
|
|
112
|
+
} catch {
|
|
113
|
+
options = {}
|
|
114
|
+
}
|
|
115
|
+
disabledGroups = new Set(Array.isArray(options.disabledGroups) ? options.disabledGroups : [])
|
|
101
116
|
await load()
|
|
102
117
|
clock = setInterval(tick, 1000)
|
|
103
118
|
bridge.onVisibility((visible) => {
|
|
@@ -161,7 +176,10 @@
|
|
|
161
176
|
frag.append(pendingCard())
|
|
162
177
|
} else if (view.configured) {
|
|
163
178
|
setStatus('已连接', 'on')
|
|
164
|
-
frag.append(
|
|
179
|
+
frag.append(tabsBar())
|
|
180
|
+
if (tab === 'market') frag.append(marketCard())
|
|
181
|
+
else if (tab === 'logs') frag.append(logsCard())
|
|
182
|
+
else frag.append(accountCard(), boardCard(), groupsCard(), topupCard(), providerCard())
|
|
165
183
|
} else {
|
|
166
184
|
setStatus('未连接')
|
|
167
185
|
frag.append(connectCard())
|
|
@@ -173,6 +191,25 @@
|
|
|
173
191
|
if (view.pending) paintCountdown()
|
|
174
192
|
}
|
|
175
193
|
|
|
194
|
+
/** tabsBar 是三个视角:原本界面、模型广场、使用日志。 */
|
|
195
|
+
function tabsBar() {
|
|
196
|
+
const tabs = [['home', '原本界面'], ['market', '模型广场'], ['logs', '使用日志']]
|
|
197
|
+
return el('div', { class: 'tabs', role: 'tablist' },
|
|
198
|
+
tabs.map(([key, label]) => el('button', {
|
|
199
|
+
role: 'tab',
|
|
200
|
+
'aria-selected': String(tab === key),
|
|
201
|
+
class: tab === key ? 'on' : '',
|
|
202
|
+
onclick: () => {
|
|
203
|
+
if (tab === key) return
|
|
204
|
+
tab = key
|
|
205
|
+
render()
|
|
206
|
+
if (key !== 'home' && view.configured && !windows.has(span)) {
|
|
207
|
+
call('neartoken:usage', { range: span }).then((w) => { windows.set(span, w); render() }).catch(showError)
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
}, label)))
|
|
211
|
+
}
|
|
212
|
+
|
|
176
213
|
/** 未连接:说清会发生什么,再给一个按钮。 */
|
|
177
214
|
function connectCard() {
|
|
178
215
|
return card('连接中转站',
|
|
@@ -330,6 +367,156 @@
|
|
|
330
367
|
return card('充值', ...parts)
|
|
331
368
|
}
|
|
332
369
|
|
|
370
|
+
/** 分组落在哪一类:低价 / 满血 / 图片 / 视频 / 语音 / 向量 / 其他。 */
|
|
371
|
+
function bucketOf(name) {
|
|
372
|
+
const n = String(name || '')
|
|
373
|
+
if (/生图|图片|image|画/i.test(n)) return '图片'
|
|
374
|
+
if (/视频|video/i.test(n)) return '视频'
|
|
375
|
+
if (/语音|tts|asr|转写|whisper|speech/i.test(n)) return '语音'
|
|
376
|
+
if (/向量|embedding/i.test(n)) return '向量'
|
|
377
|
+
if (/低价/.test(n)) return '低价'
|
|
378
|
+
if (/满血|官key|pro/i.test(n)) return '满血'
|
|
379
|
+
return '其他'
|
|
380
|
+
}
|
|
381
|
+
const BUCKET_ORDER = ['低价', '满血', '图片', '视频', '语音', '向量', '其他']
|
|
382
|
+
|
|
383
|
+
/** 模型广场:按站点返回的分组重绘,分组即为开关。 */
|
|
384
|
+
function marketCard() {
|
|
385
|
+
const list = Array.isArray(view.groupList) ? view.groupList : null
|
|
386
|
+
const head = el('div', { class: 'row' },
|
|
387
|
+
el('h2', { class: 'grow', text: '分组', style: 'margin:0' }),
|
|
388
|
+
el('a', { class: 'link', href: 'https://neartoken.cc/pricing', target: '_blank', rel: 'noreferrer' }, '站点广场 ↗'))
|
|
389
|
+
if (!list || !list.length) {
|
|
390
|
+
return card(null, head,
|
|
391
|
+
el('p', { class: 'note' },
|
|
392
|
+
'宿主还没把分组明细交给页面(`neartoken:state` 目前只回分组数)。'),
|
|
393
|
+
el('p', { class: 'note' },
|
|
394
|
+
'开关已接好:写进 magpie 的 plugin options,provider 插件按它过滤模型表;',
|
|
395
|
+
'宿主一回 groupList,这里就按站点分组重绘。先去 ',
|
|
396
|
+
el('a', { class: 'link', href: 'https://neartoken.cc/pricing', target: '_blank', rel: 'noreferrer' }, '站点广场'),
|
|
397
|
+
' 看分组与倍率。'))
|
|
398
|
+
}
|
|
399
|
+
const buckets = new Map()
|
|
400
|
+
for (const g of list) {
|
|
401
|
+
const key = bucketOf(g?.name)
|
|
402
|
+
if (!buckets.has(key)) buckets.set(key, [])
|
|
403
|
+
buckets.get(key).push(g)
|
|
404
|
+
}
|
|
405
|
+
const parts = [head]
|
|
406
|
+
for (const key of BUCKET_ORDER) {
|
|
407
|
+
const groups = buckets.get(key)
|
|
408
|
+
if (!groups || !groups.length) continue
|
|
409
|
+
const chips = groups.map((g) => groupChip(g))
|
|
410
|
+
parts.push(el('div', { class: 'bucket' },
|
|
411
|
+
el('div', { class: 'bucket-name' }, key, el('span', { class: 'n', text: String(groups.length) })),
|
|
412
|
+
el('div', { class: 'groups' }, chips)))
|
|
413
|
+
}
|
|
414
|
+
const off = disabledGroups.size
|
|
415
|
+
parts.push(el('p', { class: 'note', text: off
|
|
416
|
+
? '已关掉 ' + off + ' 个分组:它们的模型不再出现在 magpie 的模型列表里。'
|
|
417
|
+
: '全部开着。点一下关掉一组,它的模型就不再出现在 magpie 的模型列表里。' }))
|
|
418
|
+
return card(null, ...parts)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** groupChip 是一枚分组开关:站点原文组名 + 倍率 + 模型数。 */
|
|
422
|
+
function groupChip(g) {
|
|
423
|
+
const name = String(g?.name ?? '')
|
|
424
|
+
const on = !disabledGroups.has(name)
|
|
425
|
+
const ratio = Number(g?.ratio) || 0
|
|
426
|
+
const label = ratio > 0 ? name + ' ' + ratioText(ratio) + 'x' : name
|
|
427
|
+
return el('button', {
|
|
428
|
+
class: 'group-chip',
|
|
429
|
+
'aria-pressed': String(on),
|
|
430
|
+
title: g?.models ? g.models + ' 个模型' : '',
|
|
431
|
+
onclick: () => toggleGroup(name),
|
|
432
|
+
}, label, el('span', { class: 'n', text: String(g?.models || 0) }))
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** 使用日志:这个窗口里每个模型花了多少、用了多少 token。 */
|
|
436
|
+
function logsCard() {
|
|
437
|
+
const data = windows.get(span)
|
|
438
|
+
const seg = el('div', { class: 'seg' },
|
|
439
|
+
['day', 'week', 'month'].map((key) =>
|
|
440
|
+
el('button', { 'aria-pressed': String(span === key), onclick: () => switchSpan(key) },
|
|
441
|
+
key === 'day' ? '日' : key === 'week' ? '周' : '月')))
|
|
442
|
+
const head = el('div', { class: 'row' },
|
|
443
|
+
el('h2', { class: 'grow', text: '使用日志', style: 'margin:0' }), seg)
|
|
444
|
+
if (!data) return card(null, head, el('p', { class: 'note', text: '正在读取用量…' }))
|
|
445
|
+
if (!data.available) {
|
|
446
|
+
return card(null, head, el('p', { class: 'note warn', text: data.reason === 'unreachable'
|
|
447
|
+
? '暂时读不到站点的用量数据。' : '站点未提供用量统计。' }))
|
|
448
|
+
}
|
|
449
|
+
const rows = (data.models || []).slice().sort((a, b) => b.tokens - a.tokens)
|
|
450
|
+
const total = rows.reduce((sum, m) => sum + m.tokens, 0) || 1
|
|
451
|
+
const table = el('div', { class: 'log-table' },
|
|
452
|
+
el('div', { class: 'log-row head' },
|
|
453
|
+
el('span', { text: '模型' }), el('span', { text: 'Token' }), el('span', { text: '花费' }), el('span', { text: '占比' })),
|
|
454
|
+
rows.map((m) => el('div', { class: 'log-row' },
|
|
455
|
+
el('span', { class: 'who', title: m.model, text: m.model }),
|
|
456
|
+
el('span', { class: 'num', text: compact(m.tokens) }),
|
|
457
|
+
el('span', { class: 'num', text: m.costText || '' }),
|
|
458
|
+
el('span', { class: 'num', text: Math.round((m.tokens / total) * 100) + '%' }))))
|
|
459
|
+
return card(null, head,
|
|
460
|
+
el('div', { class: 'stats' },
|
|
461
|
+
el('div', { class: 'stat' }, el('div', { class: 'k', text: '消耗' }), el('div', { class: 'v', text: data.spent })),
|
|
462
|
+
el('div', { class: 'stat' }, el('div', { class: 'k', text: '当前余额' }), el('div', { class: 'v', text: data.balance || '—' })),
|
|
463
|
+
el('div', { class: 'stat' }, el('div', { class: 'k', text: 'Token' }), el('div', { class: 'v', text: compact(data.tokens) }))),
|
|
464
|
+
table,
|
|
465
|
+
el('p', { class: 'note', text: '数字来自站点用量端点;这个窗口里没有调用就不列模型。' }))
|
|
466
|
+
}
|
|
467
|
+
/** 分组墙:站点原文组名 + 倍率 + 模型数,点一下启停(写进插件 options)。 */
|
|
468
|
+
function groupsCard() {
|
|
469
|
+
const list = Array.isArray(view.groupList) ? view.groupList : null
|
|
470
|
+
const head = el('div', { class: 'row' },
|
|
471
|
+
el('h2', { class: 'grow', text: '分组', style: 'margin:0' }),
|
|
472
|
+
el('a', { class: 'link', href: 'https://neartoken.cc/pricing', target: '_blank', rel: 'noreferrer' }, '站点广场 ↗'))
|
|
473
|
+
if (!list || !list.length) {
|
|
474
|
+
return card(null, head,
|
|
475
|
+
el('p', { class: 'note' },
|
|
476
|
+
'宿主还没把分组明细交给页面。开关已经接好(写进 magpie 的 plugin options,provider 插件按它滤模型),',
|
|
477
|
+
'宿主一回 groupList,这里就是一墙可点的分组。先去 ',
|
|
478
|
+
el('a', { class: 'link', href: 'https://neartoken.cc/pricing', target: '_blank', rel: 'noreferrer' }, '站点广场'),
|
|
479
|
+
' 看分组与倍率。'))
|
|
480
|
+
}
|
|
481
|
+
const chips = list.map((g) => {
|
|
482
|
+
const name = String(g?.name ?? '')
|
|
483
|
+
const on = !disabledGroups.has(name)
|
|
484
|
+
const ratio = Number(g?.ratio) || 0
|
|
485
|
+
const label = ratio > 0 ? name + ' ' + ratioText(ratio) + 'x' : name
|
|
486
|
+
return el('button', {
|
|
487
|
+
class: 'group-chip',
|
|
488
|
+
'aria-pressed': String(on),
|
|
489
|
+
title: g?.models ? g.models + ' 个模型' : '',
|
|
490
|
+
onclick: () => toggleGroup(name),
|
|
491
|
+
}, label, el('span', { class: 'n', text: String(g?.models || 0) }))
|
|
492
|
+
})
|
|
493
|
+
const off = disabledGroups.size
|
|
494
|
+
return card(null, head,
|
|
495
|
+
el('div', { class: 'groups' }, chips),
|
|
496
|
+
el('p', { class: 'note', text: off
|
|
497
|
+
? '已关掉 ' + off + ' 个分组:它们的模型不再出现在 magpie 的模型列表里。'
|
|
498
|
+
: '全部开着。点一下关掉一组,它的模型就不再出现在 magpie 的模型列表里。' }))
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** toggleGroup 翻转一个分组,并写回插件 options(provider 插件按它过滤)。 */
|
|
502
|
+
async function toggleGroup(name) {
|
|
503
|
+
if (!name) return
|
|
504
|
+
if (disabledGroups.has(name)) disabledGroups.delete(name)
|
|
505
|
+
else disabledGroups.add(name)
|
|
506
|
+
render()
|
|
507
|
+
try {
|
|
508
|
+
const next = await bridge.options.set({ ...options, disabledGroups: [...disabledGroups] })
|
|
509
|
+
if (next && typeof next === 'object') options = next
|
|
510
|
+
} catch (error) {
|
|
511
|
+
showError(error)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** ratioText 是倍率的写法:0.1 → 0.1,1 → 1。 */
|
|
516
|
+
function ratioText(v) {
|
|
517
|
+
return String(Math.round(v * 10000) / 10000)
|
|
518
|
+
}
|
|
519
|
+
|
|
333
520
|
/** 供应商卡:说明模型已经交给 magpie 了。 */
|
|
334
521
|
function providerCard() {
|
|
335
522
|
const models = view.models || 0
|
package/page/style.css
CHANGED
|
@@ -382,4 +382,69 @@ select {
|
|
|
382
382
|
clip: rect(0 0 0 0);
|
|
383
383
|
white-space: nowrap;
|
|
384
384
|
border: 0;
|
|
385
|
-
}
|
|
385
|
+
}
|
|
386
|
+
/* 分组墙:像模型广场那样成片的组名与倍率 */
|
|
387
|
+
.groups { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 4px; }
|
|
388
|
+
|
|
389
|
+
.group-chip {
|
|
390
|
+
display: inline-flex;
|
|
391
|
+
align-items: baseline;
|
|
392
|
+
gap: 7px;
|
|
393
|
+
height: 30px;
|
|
394
|
+
padding: 0 11px;
|
|
395
|
+
border-radius: 999px;
|
|
396
|
+
border: 1px solid var(--nt-line);
|
|
397
|
+
background: transparent;
|
|
398
|
+
color: var(--nt-muted);
|
|
399
|
+
font-size: 12px;
|
|
400
|
+
white-space: nowrap;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.group-chip[aria-pressed="true"] {
|
|
404
|
+
color: var(--nt-ink);
|
|
405
|
+
border-color: var(--nt-accent);
|
|
406
|
+
background: var(--nt-accent-soft);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.group-chip .n {
|
|
410
|
+
font-family: var(--nt-code);
|
|
411
|
+
font-size: 10.5px;
|
|
412
|
+
color: var(--nt-faint);
|
|
413
|
+
font-variant-numeric: tabular-nums;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.group-chip[aria-pressed="true"] .n { color: var(--nt-muted); }
|
|
417
|
+
|
|
418
|
+
.link { color: var(--nt-accent); text-decoration: none; font-size: 11.5px; }
|
|
419
|
+
.link:hover { text-decoration: underline; }
|
|
420
|
+
/* 三个视角:原本界面 / 模型广场 / 使用日志 */
|
|
421
|
+
.tabs { display: inline-flex; gap: 2px; padding: 3px; margin: 14px 0 0; border: 1px solid var(--nt-line); border-radius: 10px; }
|
|
422
|
+
|
|
423
|
+
.tabs button {
|
|
424
|
+
border: 0; height: 26px; padding: 0 12px; border-radius: 8px;
|
|
425
|
+
color: var(--nt-muted); background: transparent; font-size: 12px;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.tabs button.on { color: var(--nt-ink); background: var(--nt-accent-soft); box-shadow: inset 0 0 0 1px var(--nt-accent); }
|
|
429
|
+
|
|
430
|
+
/* 模型广场:一类一组,组名在上,开关在下 */
|
|
431
|
+
.bucket { margin: 14px 0 0; }
|
|
432
|
+
.bucket-name { display: flex; align-items: baseline; gap: 7px; font-size: 11.5px; color: var(--nt-muted); margin-bottom: 8px; }
|
|
433
|
+
.bucket-name .n { font-family: var(--nt-code); font-size: 10.5px; color: var(--nt-faint); }
|
|
434
|
+
|
|
435
|
+
/* 使用日志 */
|
|
436
|
+
.log-table { margin-top: 12px; border-top: 1px solid var(--nt-line); }
|
|
437
|
+
|
|
438
|
+
.log-row {
|
|
439
|
+
display: grid;
|
|
440
|
+
grid-template-columns: 1fr 84px 92px 56px;
|
|
441
|
+
gap: 8px;
|
|
442
|
+
padding: 7px 0;
|
|
443
|
+
border-bottom: 1px solid var(--nt-line);
|
|
444
|
+
font-size: 12px;
|
|
445
|
+
align-items: baseline;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.log-row.head { color: var(--nt-muted); font-size: 11px; border-bottom-color: transparent; }
|
|
449
|
+
.log-row .who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
450
|
+
.log-row .num { text-align: right; font-family: var(--nt-code); font-variant-numeric: tabular-nums; color: var(--nt-muted); }
|