dsh-plugin-panel 0.1.1 → 0.1.3

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/client.js CHANGED
@@ -6,7 +6,7 @@ window.__ModuleLoader__.load({
6
6
  const PANEL_ID = 'dsh-plugin-panel'
7
7
  const TAB_KIND = 'plugin-canvas'
8
8
  const NS = 'dsh-plugin-panel'
9
- const VERSION = 'v0.1.1'
9
+ const VERSION = 'v0.1.3'
10
10
  const PREF_KEY = 'dsh.plugin-list.pref.v2'
11
11
 
12
12
  const COPY = {
@@ -16,8 +16,45 @@ window.__ModuleLoader__.load({
16
16
  'guide.description': '查看并打开右侧栏插件。',
17
17
  'list.installed': '用户安装',
18
18
  'list.builtin': '系统内置',
19
- 'list.all': '全部',
20
19
  'list.refresh': '刷新',
20
+ 'list.market': '市场',
21
+ 'list.back': '返回',
22
+ 'list.marketTitle': '插件市场趋势',
23
+ 'list.marketHint': '来自 dsh-plugin-shop 目录的每日快照,不是 npm 全站统计。',
24
+ 'list.marketLoading': '正在取历史数据,第一次大约十几秒…',
25
+ 'list.marketError': '取不到目录数据。如果刚更新过插件,重启一次 DeepSeek Harness 再试。',
26
+ 'list.countNow': '当前收录',
27
+ 'list.countFiltered': '已过滤',
28
+ 'list.perDay': '日均新增',
29
+ 'list.spanDays': '覆盖天数',
30
+ 'list.marketFirstNote': '首日已含此前录入的存量,不是从 0 开始',
31
+ 'list.install': '安装',
32
+ 'list.uninstall': '卸载',
33
+ 'list.uninstallConfirm': '确认卸载',
34
+ 'list.installing': '安装中…',
35
+ 'list.removing': '卸载中…',
36
+ 'list.hotDelta': '7 天增长',
37
+ 'list.hotStars': '仓库 star',
38
+ 'list.hotCategory': '分类',
39
+ 'list.hotRepo': '仓库',
40
+ 'list.installedState': '已安装',
41
+ 'list.notInstalled': '未安装',
42
+ 'list.chartTitle': '插件市场的增长趋势',
43
+ 'list.chartCumulative': '累计',
44
+ 'list.chartDaily': '每日增量',
45
+ 'list.chartDailyHint': '每天新增多少,比累计更能看出快慢',
46
+ 'list.marketSearch': '搜索整个市场的插件…',
47
+ 'list.marketSearching': '搜索中…',
48
+ 'list.searchError': '搜索暂时不可用。如果刚更新过插件,在插件管理里停用再启用一次即可。',
49
+ 'list.marketNoMatch': '没有找到匹配的插件',
50
+ 'list.marketHits': '找到 {n} 个',
51
+ 'list.hotSearch': '搜索插件名、说明或分类…',
52
+ 'list.hotNoMatch': '没有匹配的插件',
53
+ 'list.hotTitle': '最近 7 天增长最快',
54
+ 'list.hotHint': '按 GitHub 仓库 star 的 7 天增量排序,同一仓库只算一次。',
55
+ 'list.hotLoading': '正在算增长…',
56
+ 'list.hotError': '取不到增长数据。如果刚更新插件,需要重启一次 DeepSeek Harness 才会启用这个榜单。',
57
+ 'list.hotEmpty': '这 7 天没有增长记录。',
21
58
  'list.viewList': '列表',
22
59
  'list.viewCard': '卡片',
23
60
  'list.open': '打开',
@@ -79,8 +116,45 @@ window.__ModuleLoader__.load({
79
116
  'guide.description': 'Browse and open right-sidebar plugins.',
80
117
  'list.installed': 'Installed',
81
118
  'list.builtin': 'Built-in',
82
- 'list.all': 'All',
83
119
  'list.refresh': 'Refresh',
120
+ 'list.market': 'Market',
121
+ 'list.back': 'Back',
122
+ 'list.marketTitle': 'Plugin market trend',
123
+ 'list.marketHint': 'Daily snapshots of the dsh-plugin-shop catalog, not a site-wide npm count.',
124
+ 'list.marketLoading': 'Fetching history, about ten seconds the first time…',
125
+ 'list.marketError': 'Could not read the catalog. Restart DeepSeek Harness once if you just updated.',
126
+ 'list.countNow': 'Listed',
127
+ 'list.countFiltered': 'Filtered',
128
+ 'list.perDay': 'Per day',
129
+ 'list.spanDays': 'Days',
130
+ 'list.marketFirstNote': 'First point already includes earlier stock; not growth from zero',
131
+ 'list.install': 'Install',
132
+ 'list.uninstall': 'Remove',
133
+ 'list.uninstallConfirm': 'Confirm',
134
+ 'list.installing': 'Installing…',
135
+ 'list.removing': 'Removing…',
136
+ 'list.hotDelta': '7-day growth',
137
+ 'list.hotStars': 'Repo stars',
138
+ 'list.hotCategory': 'Category',
139
+ 'list.hotRepo': 'Repository',
140
+ 'list.installedState': 'Installed',
141
+ 'list.notInstalled': 'Not installed',
142
+ 'list.chartTitle': 'Plugin market growth',
143
+ 'list.chartCumulative': 'Total',
144
+ 'list.chartDaily': 'Per day',
145
+ 'list.chartDailyHint': 'New plugins per day, which shows pace better than a total',
146
+ 'list.marketSearch': 'Search the whole market…',
147
+ 'list.marketSearching': 'Searching…',
148
+ 'list.searchError': 'Search is unavailable. If you just updated the plugin, disable and re-enable it once.',
149
+ 'list.marketNoMatch': 'No matching plugin',
150
+ 'list.marketHits': '{n} found',
151
+ 'list.hotSearch': 'Search by name, description or category…',
152
+ 'list.hotNoMatch': 'No matching plugin',
153
+ 'list.hotTitle': 'Fastest growing, last 7 days',
154
+ 'list.hotHint': 'Ranked by 7-day GitHub repository star delta; one row per repository.',
155
+ 'list.hotLoading': 'Crunching growth…',
156
+ 'list.hotError': 'Could not load growth data. Restart DeepSeek Harness once to enable this ranking.',
157
+ 'list.hotEmpty': 'No growth recorded in the last 7 days.',
84
158
  'list.viewList': 'List',
85
159
  'list.viewCard': 'Cards',
86
160
  'list.open': 'Open',
@@ -151,7 +225,7 @@ window.__ModuleLoader__.load({
151
225
  try {
152
226
  const parsed = JSON.parse(window.localStorage.getItem(PREF_KEY) || '{}')
153
227
  return {
154
- filter: ['installed', 'builtin', 'all'].includes(parsed.filter) ? parsed.filter : 'installed',
228
+ filter: parsed.filter === 'builtin' ? 'builtin' : 'installed',
155
229
  pins: Array.isArray(parsed.pins) ? parsed.pins.filter((id) => typeof id === 'string') : [],
156
230
  layout: parsed.layout === 'card' ? 'card' : 'list',
157
231
  }
@@ -253,6 +327,169 @@ window.__ModuleLoader__.load({
253
327
  return ''
254
328
  }
255
329
 
330
+ /** Bars for the day-over-day change; the first day has no previous snapshot. */
331
+ function barChart(days, hoverIndex, onHover) {
332
+ const W = 1000
333
+ const H = 250
334
+ const L = 68
335
+ const R = 24
336
+ const T = 18
337
+ const B = 42
338
+ const deltas = days.map((day, index) => (index === 0 ? null : day.count - days[index - 1].count))
339
+ const usable = deltas.filter((value) => typeof value === 'number')
340
+ const peak = Math.max(...usable, 1) * 1.15
341
+ const n = days.length
342
+ const slot = (W - L - R) / Math.max(n, 1)
343
+ const barW = Math.max(2, Math.min(slot * 0.62, 14))
344
+ const x = (i) => L + slot * i + slot / 2
345
+ const zeroY = T + (H - T - B)
346
+ const y = (v) => zeroY - ((H - T - B) * v) / Math.max(peak, 1)
347
+ const ticks = [0, 0.5, 1].map((r) => Math.round((peak * r) / 500) * 500)
348
+ const labelIndexes = n <= 1 ? [0] : [0, Math.floor((n - 1) / 2), n - 1]
349
+
350
+ const pick = (event) => {
351
+ const rect = event.currentTarget.getBoundingClientRect()
352
+ if (!rect.width) return
353
+ const px = ((event.clientX - rect.left) / rect.width) * W
354
+ const index = Math.round((px - L - slot / 2) / Math.max(slot, 1))
355
+ onHover(Math.max(0, Math.min(n - 1, index)))
356
+ }
357
+
358
+ const hovered = hoverIndex >= 0 && hoverIndex < n ? hoverIndex : -1
359
+ const hx = hovered >= 0 ? x(hovered) : 0
360
+ const value = hovered >= 0 ? deltas[hovered] : null
361
+ const boxW = 200
362
+ const boxH = 64
363
+ const boxX = hovered >= 0 ? Math.min(Math.max(hx - boxW / 2, L), W - R - boxW) : 0
364
+ const boxY = hovered >= 0 ? Math.max(T + 8, y(typeof value === 'number' ? value : 0) - boxH - 14) : 0
365
+ const shown = typeof value === 'number' ? `${value >= 0 ? '+' : ''}${value.toLocaleString()}` : '—'
366
+
367
+ return h('svg', {
368
+ className: 'dsh-pl-chart',
369
+ viewBox: `0 0 ${W} ${H}`,
370
+ role: 'img',
371
+ onMouseMove: pick,
372
+ onMouseLeave: () => onHover(-1),
373
+ },
374
+ ticks.map((tick, i) => h('line', {
375
+ key: `g${i}`, x1: L, x2: W - R, y1: y(tick), y2: y(tick),
376
+ stroke: 'var(--dsw-alias-border-l1)', strokeWidth: 1,
377
+ })),
378
+ ticks.map((tick, i) => h('text', {
379
+ key: `t${i}`, x: L - 12, y: y(tick) + 6, textAnchor: 'end',
380
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
381
+ }, `+${Math.round(tick / 1000)}k`)),
382
+ deltas.map((delta, i) => {
383
+ if (typeof delta !== 'number') return null
384
+ const top = Math.min(y(delta), zeroY)
385
+ const height = Math.max(1, Math.abs(zeroY - y(delta)))
386
+ return h('rect', {
387
+ key: `b${i}`, x: x(i) - barW / 2, y: top, width: barW, height,
388
+ rx: Math.min(2, barW / 2),
389
+ fill: 'var(--dsw-alias-brand-primary)',
390
+ opacity: hovered === i ? 0.95 : 0.55,
391
+ })
392
+ }),
393
+ labelIndexes.map((i) => h('text', {
394
+ key: `d${i}`, x: x(i), y: H - B + 34, textAnchor: i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle',
395
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
396
+ }, days[i].day.slice(5))),
397
+ hovered >= 0 ? h('g', null,
398
+ h('line', {
399
+ x1: hx, x2: hx, y1: T, y2: H - B,
400
+ stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 1, strokeDasharray: '4 4', opacity: 0.5,
401
+ }),
402
+ h('rect', {
403
+ x: boxX, y: boxY, width: boxW, height: boxH, rx: 10,
404
+ fill: 'var(--dsw-alias-bg-overlay)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 1,
405
+ }),
406
+ h('text', { x: boxX + 14, y: boxY + 24, fill: 'var(--dsw-alias-label-secondary)', fontSize: 16 }, days[hovered].day),
407
+ h('text', { x: boxX + 14, y: boxY + 50, fill: 'var(--dsw-alias-label-primary)', fontSize: 22, fontWeight: 600 }, shown),
408
+ ) : null,
409
+ )
410
+ }
411
+
412
+ function trendChart(days, hoverIndex, onHover, firstNote) {
413
+ const W = 1000
414
+ const H = 250
415
+ const L = 68
416
+ const R = 24
417
+ const T = 18
418
+ const B = 42
419
+ const values = days.map((d) => d.count)
420
+ const max = Math.max(...values, 1) * 1.2
421
+ const n = days.length
422
+ const x = (i) => L + (W - L - R) * (n <= 1 ? 0.5 : i / (n - 1))
423
+ const y = (v) => T + (H - T - B) * (1 - v / max)
424
+ const line = days.map((d, i) => `${i === 0 ? 'M' : 'L'} ${x(i).toFixed(1)} ${y(d.count).toFixed(1)}`).join(' ')
425
+ const area = `${line} L ${x(n - 1).toFixed(1)} ${H - B} L ${x(0).toFixed(1)} ${H - B} Z`
426
+ const ticks = [0, 0.25, 0.5, 0.75, 1].map((r) => Math.round((max * r) / 1000) * 1000)
427
+ const labelIndexes = n <= 1 ? [0] : [0, Math.floor((n - 1) / 2), n - 1]
428
+
429
+ const pick = (event) => {
430
+ const rect = event.currentTarget.getBoundingClientRect()
431
+ if (!rect.width) return
432
+ const px = ((event.clientX - rect.left) / rect.width) * W
433
+ const ratio = (px - L) / Math.max(W - L - R, 1)
434
+ const index = Math.round(ratio * Math.max(n - 1, 1))
435
+ onHover(Math.max(0, Math.min(n - 1, index)))
436
+ }
437
+
438
+ const hovered = hoverIndex >= 0 && hoverIndex < n ? hoverIndex : -1
439
+ const hx = hovered >= 0 ? x(hovered) : 0
440
+ const hy = hovered >= 0 ? y(values[hovered]) : 0
441
+ const isFirst = hovered === 0
442
+ const boxW = isFirst ? 246 : 196
443
+ const boxH = isFirst ? 96 : 64
444
+ const boxX = hovered >= 0 ? Math.min(Math.max(hx - boxW / 2, L), W - R - boxW) : 0
445
+ const boxY = hovered >= 0 ? Math.max(hy - boxH - 16, T) : 0
446
+
447
+ return h('svg', {
448
+ className: 'dsh-pl-chart',
449
+ viewBox: `0 0 ${W} ${H}`,
450
+ role: 'img',
451
+ onMouseMove: pick,
452
+ onMouseLeave: () => onHover(-1),
453
+ },
454
+ ticks.map((tick, i) => h('line', {
455
+ key: `g${i}`, x1: L, x2: W - R, y1: y(tick), y2: y(tick),
456
+ stroke: 'var(--dsw-alias-border-l1)', strokeWidth: 1,
457
+ })),
458
+ ticks.map((tick, i) => h('text', {
459
+ key: `t${i}`, x: L - 12, y: y(tick) + 6, textAnchor: 'end',
460
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
461
+ }, `${Math.round(tick / 1000)}k`)),
462
+ h('path', { d: area, fill: 'var(--dsw-alias-brand-primary)', opacity: 0.08 }),
463
+ h('path', { d: line, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }),
464
+ labelIndexes.map((i) => h('text', {
465
+ key: `d${i}`, x: x(i), y: H - B + 34, textAnchor: i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle',
466
+ fill: 'var(--dsw-alias-label-secondary)', fontSize: 16,
467
+ }, days[i].day.slice(5))),
468
+ h('text', {
469
+ x: x(n - 1), y: y(values[n - 1]) - 16, textAnchor: 'end',
470
+ fill: 'var(--dsw-alias-label-primary)', fontSize: 22, fontWeight: 600,
471
+ }, String(values[n - 1])),
472
+ hovered >= 0 ? h('g', null,
473
+ h('line', {
474
+ x1: hx, x2: hx, y1: T, y2: H - B,
475
+ stroke: 'var(--dsw-alias-label-secondary)', strokeWidth: 1, strokeDasharray: '4 4', opacity: 0.5,
476
+ }),
477
+ h('circle', { cx: hx, cy: hy, r: 4.5, fill: 'var(--dsw-alias-brand-primary)', stroke: '#fff', strokeWidth: 2 }),
478
+ h('rect', {
479
+ x: boxX, y: boxY, width: boxW, height: boxH, rx: 10,
480
+ fill: 'var(--dsw-alias-bg-overlay)', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 1,
481
+ }),
482
+ h('text', { x: boxX + 14, y: boxY + 24, fill: 'var(--dsw-alias-label-secondary)', fontSize: 16 },
483
+ days[hovered].day),
484
+ h('text', { x: boxX + 14, y: boxY + 50, fill: 'var(--dsw-alias-label-primary)', fontSize: 22, fontWeight: 600 },
485
+ `${values[hovered].toLocaleString()} 个`),
486
+ isFirst && firstNote
487
+ ? h('text', { x: boxX + 14, y: boxY + 78, fill: 'var(--dsw-alias-label-secondary)', fontSize: 14 }, firstNote)
488
+ : null,
489
+ ) : null,
490
+ )
491
+ }
492
+
256
493
  function detailNames(plugin, bundle) {
257
494
  const names = []
258
495
  const moduleName = String((plugin && plugin.moduleName) || '')
@@ -410,6 +647,49 @@ window.__ModuleLoader__.load({
410
647
  }) || null
411
648
  }
412
649
 
650
+ /** One card shape, shared by the plugin list and the market ranking. */
651
+ function PluginCard(options) {
652
+ const { icon, title, subtitle, actions, className, onClick, badge } = options
653
+ const classes = ['dsh-pl-item']
654
+ if (className) classes.push(className)
655
+ return h('div', {
656
+ className: classes.join(' '),
657
+ role: 'button',
658
+ tabIndex: 0,
659
+ onClick,
660
+ onKeyDown: (event) => {
661
+ if (event.key === 'Enter' || event.key === ' ') {
662
+ event.preventDefault()
663
+ if (onClick) onClick()
664
+ }
665
+ },
666
+ },
667
+ icon || h(DefaultPluginIcon),
668
+ h('span', { className: 'dsh-pl-copy' },
669
+ h('strong', null, title),
670
+ subtitle ? h('span', { className: 'dsh-pl-where' }, subtitle) : null),
671
+ badge || null,
672
+ actions && actions.length ? h('div', { className: 'dsh-pl-acts' }, actions) : null)
673
+ }
674
+
675
+ /** One detail shape, shared by the plugin detail and the market detail. */
676
+ function DetailPanel(options) {
677
+ const { icon, title, facts, onClose, closeLabel, actions, note, children } = options
678
+ const rows = (facts || []).filter((entry) => entry && entry.value)
679
+ return h('aside', { className: 'dsh-pl-detail' },
680
+ h('div', { className: 'dsh-pl-detail-head' },
681
+ icon || h(DefaultPluginIcon),
682
+ h('strong', null, title),
683
+ h('button', { type: 'button', onClick: onClose }, closeLabel)),
684
+ h('div', { className: 'dsh-pl-detail-body' },
685
+ rows.map((entry, index) => h('div', { className: 'dsh-pl-fact', key: index },
686
+ h('span', { className: 'dsh-pl-fact-label' }, entry.label),
687
+ h('span', { className: 'dsh-pl-fact-value' }, entry.value))),
688
+ note ? h('p', { className: 'dsh-pl-note' }, note) : null,
689
+ children || null,
690
+ actions && actions.length ? h('div', { className: 'dsh-pl-actions' }, actions) : null))
691
+ }
692
+
413
693
  function DefaultPluginIcon() {
414
694
  return h('svg', {
415
695
  className: 'dsh-pl-fallback',
@@ -449,6 +729,22 @@ window.__ModuleLoader__.load({
449
729
  const [filter, setFilter] = React.useState(pref.current.filter)
450
730
  const [pins, setPins] = React.useState(pref.current.pins || [])
451
731
  const [view, setView] = React.useState(pref.current.layout === 'card' ? 'card' : 'list')
732
+ const [surface, setSurface] = React.useState('plugins')
733
+ const [trend, setTrend] = React.useState(null)
734
+ const [trendStatus, setTrendStatus] = React.useState('')
735
+ const [hoverIndex, setHoverIndex] = React.useState(-1)
736
+ const [hot, setHot] = React.useState(null)
737
+ const [hotError, setHotError] = React.useState('')
738
+ const [busyKey, setBusyKey] = React.useState('')
739
+ const [confirmKey, setConfirmKey] = React.useState('')
740
+ const [selectedMarket, setSelectedMarket] = React.useState(null)
741
+ const [chartMode, setChartMode] = React.useState('cumulative')
742
+ const [hotQuery, setHotQuery] = React.useState('')
743
+ const [marketQuery, setMarketQuery] = React.useState('')
744
+ const [searchResult, setSearchResult] = React.useState(null)
745
+ const [searchStatus, setSearchStatus] = React.useState('')
746
+ const [trendError, setTrendError] = React.useState('')
747
+ const [hotStatus, setHotStatus] = React.useState('')
452
748
  const [plugins, setPlugins] = React.useState([])
453
749
  const [bundles, setBundles] = React.useState([])
454
750
  const [status, setStatus] = React.useState(listPlugins ? 'loading' : 'unavailable')
@@ -574,12 +870,100 @@ window.__ModuleLoader__.load({
574
870
  return () => { cancelled = true }
575
871
  }, [selectedId, plugins, bundles, loadFacts])
576
872
 
873
+ const MARKET_CACHE_KEY = 'dsh.plugin-list.market.v1'
874
+
875
+ function todayKey() {
876
+ return new Date().toISOString().slice(0, 10)
877
+ }
878
+
879
+ function readMarketCache() {
880
+ try {
881
+ const raw = JSON.parse(window.localStorage.getItem(MARKET_CACHE_KEY) || '{}')
882
+ return raw && raw.day === todayKey() ? raw : null
883
+ } catch {
884
+ return null
885
+ }
886
+ }
887
+
888
+ function writeMarketCache(patch) {
889
+ try {
890
+ const raw = readMarketCache() || { day: todayKey() }
891
+ window.localStorage.setItem(MARKET_CACHE_KEY, JSON.stringify({ ...raw, ...patch }))
892
+ } catch { /* caching is best effort */ }
893
+ }
894
+
895
+ const loadTrend = React.useCallback(async () => {
896
+ const cached = readMarketCache()
897
+ if (cached && cached.trend) {
898
+ setTrend(cached.trend)
899
+ setTrendStatus('ready')
900
+ return
901
+ }
902
+ const call = props.loadMarketTrend
903
+ if (typeof call !== 'function') return
904
+ setTrendStatus('loading')
905
+ try {
906
+ const result = await call()
907
+ if (result && result.ok === false) {
908
+ setTrendError((result.error && result.error.message) || '')
909
+ setTrendStatus('error')
910
+ return
911
+ }
912
+ const value = result && result.ok === true ? result.value : result
913
+ if (value && Array.isArray(value.days) && value.days.length) {
914
+ setTrend(value)
915
+ writeMarketCache({ trend: value })
916
+ setTrendStatus('ready')
917
+ } else {
918
+ setTrendStatus('error')
919
+ }
920
+ } catch (failure) {
921
+ setTrendError(failure instanceof Error ? failure.message : '')
922
+ setTrendStatus('error')
923
+ }
924
+ }, [props.loadMarketTrend])
925
+
926
+ const loadHot = React.useCallback(async () => {
927
+ const cached = readMarketCache()
928
+ if (cached && cached.hot) {
929
+ setHot(cached.hot)
930
+ setHotStatus('ready')
931
+ return
932
+ }
933
+ const call = props.loadMarketHot
934
+ if (typeof call !== 'function') return
935
+ setHotStatus('loading')
936
+ try {
937
+ const result = await call()
938
+ if (result && result.ok === false) {
939
+ setHotError((result.error && result.error.message) || '')
940
+ setHotStatus('error')
941
+ return
942
+ }
943
+ const value = result && result.ok === true ? result.value : result
944
+ if (value && Array.isArray(value.top) && value.top.length) {
945
+ setHot(value)
946
+ writeMarketCache({ hot: value })
947
+ setHotStatus('ready')
948
+ } else {
949
+ setHotStatus('empty')
950
+ }
951
+ } catch (failure) {
952
+ setHotError(failure instanceof Error ? failure.message : '')
953
+ setHotStatus('error')
954
+ }
955
+ }, [props.loadMarketHot])
956
+
957
+ React.useEffect(() => {
958
+ if (surface !== 'market') return
959
+ if (trendStatus === '') void loadTrend()
960
+ if (hotStatus === '') void loadHot()
961
+ }, [surface, trendStatus, hotStatus, loadTrend, loadHot])
962
+
577
963
  const visible = React.useMemo(() => {
578
964
  const rows = plugins.filter((plugin) => {
579
965
  const origin = originOf(plugin)
580
- if (filter === 'installed') return origin === 'installed'
581
- if (filter === 'builtin') return origin === 'builtin'
582
- return true
966
+ return filter === 'builtin' ? origin === 'builtin' : origin === 'installed'
583
967
  })
584
968
  const rank = (plugin) => {
585
969
  const places = placesFor(plugin, guide, seats, localeId)
@@ -599,13 +983,6 @@ window.__ModuleLoader__.load({
599
983
  const selected = plugins.find((item) => item.entryId === selectedId) || null
600
984
  const selectedBundle = selected ? matchBundle(selected, bundles) : null
601
985
 
602
- function fact(label, value) {
603
- if (!value) return null
604
- return h('div', { className: 'dsh-pl-fact' },
605
- h('span', { className: 'dsh-pl-fact-label' }, label),
606
- h('span', { className: 'dsh-pl-fact-value' }, value))
607
- }
608
-
609
986
  function revealHint(text) {
610
987
  const raw = String(text || '')
611
988
  if (/Unknown detail method|invalid server-response|unaddressable|not found/i.test(raw)) return textOf(t, 'list.revealRestart')
@@ -640,64 +1017,73 @@ window.__ModuleLoader__.load({
640
1017
  const componentText = rows.length
641
1018
  ? `${rows.length}${siblings.length ? ` · ${siblings.join('、')}${rows.length > 7 ? '…' : ''}` : ''}`
642
1019
  : ''
643
- return h('aside', { className: 'dsh-pl-detail' },
644
- h('div', { className: 'dsh-pl-detail-head' },
645
- icon ? h('img', { src: icon, alt: '' }) : h(DefaultPluginIcon),
646
- h('strong', null, title),
647
- h('button', { type: 'button', onClick: () => setSelectedId('') }, textOf(t, 'list.close'))),
648
- h('div', { className: 'dsh-pl-detail-body' },
649
- fact(textOf(t, 'list.intro'), intro || textOf(t, 'list.unknown')),
650
- fact(textOf(t, 'list.module'), shortModule(plugin.moduleName)),
651
- fact(textOf(t, 'list.entry'), plugin.entryId),
652
- fact(textOf(t, 'list.status'), textOf(t, phaseKey(plugin))),
653
- fact(textOf(t, 'list.package'), (selectedBundle && selectedBundle.name) || (manifest && manifest.name) || shortModule(plugin.moduleName)),
654
- fact(textOf(t, 'list.version'), (selectedBundle && selectedBundle.version) || (manifest && manifest.version)),
655
- fact(textOf(t, 'list.place'), place),
656
- fact(textOf(t, 'list.source'), source),
657
- fact(textOf(t, 'list.flags'), flags),
658
- fact(textOf(t, 'list.components'), componentText),
659
- fact(textOf(t, 'list.license'), manifest && manifest.license),
660
- fact(textOf(t, 'list.repo'), manifest && manifest.repository),
661
- fact(textOf(t, 'list.engines'), manifest && manifest.engines),
662
- fact(textOf(t, 'list.keywords'), manifest && Array.isArray(manifest.keywords) ? manifest.keywords.join('、') : ''),
663
- fact(textOf(t, 'list.inject'), manifest && Array.isArray(manifest.clientInject) ? manifest.clientInject.join('、') : ''),
664
- h('section', { className: 'dsh-pl-doc-block' },
665
- h('div', { className: 'dsh-pl-fact-label' }, textOf(t, 'list.doc')),
666
- readmeStatus === 'loading'
667
- ? h('p', null, textOf(t, 'list.docLoading'))
668
- : (readme || (facts && facts.document))
669
- ? markdownView(readme || facts.document)
670
- : h('p', null, textOf(t, 'list.docEmpty'))),
671
- h('div', { className: 'dsh-pl-actions' },
672
- tabKindFor(plugin, guide, localeId) && typeof openPanel === 'function'
673
- ? h('button', {
674
- type: 'button',
675
- onClick: () => openPanel(tabKindFor(plugin, guide, localeId)),
676
- }, textOf(t, 'list.open'))
677
- : null,
678
- typeof props.revealPackage === 'function'
679
- ? h('button', {
680
- type: 'button',
681
- onClick: () => {
682
- setRevealNote('')
683
- const source = selectedBundle && selectedBundle.source
684
- Promise.resolve(props.revealPackage({ moduleName: plugin.moduleName || '', source: source || '' })).then((result) => {
685
- if (result && result.ok === false) setRevealNote(revealHint(result.error && result.error.message))
686
- }).catch((failure) => {
687
- setRevealNote(revealHint(failure instanceof Error ? failure.message : ''))
688
- })
689
- },
690
- }, textOf(t, 'list.reveal'))
691
- : null,
692
- plugin.patchId && plugin.readOnlyReason == null && setPluginEnabled
693
- ? h('button', {
694
- type: 'button',
695
- onClick: () => { void setPluginEnabled(plugin.entryId, plugin.enabled !== true).then(() => load()) },
696
- }, plugin.enabled ? textOf(t, 'list.disable') : textOf(t, 'list.enable'))
697
- : plugin.readOnlyReason
698
- ? h('span', { className: 'dsh-pl-note' }, String(plugin.readOnlyReason))
699
- : h('span', { className: 'dsh-pl-note' }, textOf(t, 'list.locked')),
700
- revealNote ? h('span', { className: 'dsh-pl-note' }, revealNote) : null)))
1020
+ const detailFacts = [
1021
+ { label: textOf(t, 'list.intro'), value: intro || textOf(t, 'list.unknown') },
1022
+ { label: textOf(t, 'list.module'), value: shortModule(plugin.moduleName) },
1023
+ { label: textOf(t, 'list.entry'), value: plugin.entryId },
1024
+ { label: textOf(t, 'list.status'), value: textOf(t, phaseKey(plugin)) },
1025
+ { label: textOf(t, 'list.package'), value: (selectedBundle && selectedBundle.name) || (manifest && manifest.name) || shortModule(plugin.moduleName) },
1026
+ { label: textOf(t, 'list.version'), value: (selectedBundle && selectedBundle.version) || (manifest && manifest.version) },
1027
+ { label: textOf(t, 'list.place'), value: place },
1028
+ { label: textOf(t, 'list.source'), value: source },
1029
+ { label: textOf(t, 'list.flags'), value: flags },
1030
+ { label: textOf(t, 'list.components'), value: componentText },
1031
+ { label: textOf(t, 'list.license'), value: manifest && manifest.license },
1032
+ { label: textOf(t, 'list.repo'), value: manifest && manifest.repository },
1033
+ { label: textOf(t, 'list.engines'), value: manifest && manifest.engines },
1034
+ { label: textOf(t, 'list.keywords'), value: manifest && Array.isArray(manifest.keywords) ? manifest.keywords.join('、') : '' },
1035
+ { label: textOf(t, 'list.inject'), value: manifest && Array.isArray(manifest.clientInject) ? manifest.clientInject.join('、') : '' },
1036
+ ]
1037
+
1038
+ const detailActions = []
1039
+ const openKind = tabKindFor(plugin, guide, localeId)
1040
+ if (openKind && typeof openPanel === 'function') {
1041
+ detailActions.push(h('button', {
1042
+ key: 'open',
1043
+ type: 'button',
1044
+ onClick: () => openPanel(openKind),
1045
+ }, textOf(t, 'list.open')))
1046
+ }
1047
+ if (typeof props.revealPackage === 'function') {
1048
+ detailActions.push(h('button', {
1049
+ key: 'reveal',
1050
+ type: 'button',
1051
+ onClick: () => {
1052
+ setRevealNote('')
1053
+ const source = selectedBundle && selectedBundle.source
1054
+ Promise.resolve(props.revealPackage({ moduleName: plugin.moduleName || '', source: source || '' })).then((result) => {
1055
+ if (result && result.ok === false) setRevealNote(revealHint(result.error && result.error.message))
1056
+ }).catch((failure) => {
1057
+ setRevealNote(revealHint(failure instanceof Error ? failure.message : ''))
1058
+ })
1059
+ },
1060
+ }, textOf(t, 'list.reveal')))
1061
+ }
1062
+ if (plugin.patchId && plugin.readOnlyReason == null && setPluginEnabled) {
1063
+ detailActions.push(h('button', {
1064
+ key: 'toggle',
1065
+ type: 'button',
1066
+ onClick: () => { void setPluginEnabled(plugin.entryId, plugin.enabled !== true).then(() => load()) },
1067
+ }, plugin.enabled ? textOf(t, 'list.disable') : textOf(t, 'list.enable')))
1068
+ }
1069
+ if (revealNote) detailActions.push(h('span', { key: 'note', className: 'dsh-pl-note' }, revealNote))
1070
+ if (plugin.readOnlyReason) detailActions.push(h('span', { key: 'locked', className: 'dsh-pl-note' }, String(plugin.readOnlyReason)))
1071
+
1072
+ return h(DetailPanel, {
1073
+ icon: icon ? h('img', { src: icon, alt: '' }) : null,
1074
+ title,
1075
+ closeLabel: textOf(t, 'list.close'),
1076
+ onClose: () => setSelectedId(''),
1077
+ facts: detailFacts,
1078
+ actions: detailActions,
1079
+ children: h('section', { className: 'dsh-pl-doc-block' },
1080
+ h('div', { className: 'dsh-pl-fact-label' }, textOf(t, 'list.doc')),
1081
+ readmeStatus === 'loading'
1082
+ ? h('p', null, textOf(t, 'list.docLoading'))
1083
+ : (readme || (facts && facts.document))
1084
+ ? markdownView(readme || facts.document)
1085
+ : h('p', null, textOf(t, 'list.docEmpty'))),
1086
+ })
701
1087
  }
702
1088
 
703
1089
  function togglePin(entryId) {
@@ -719,20 +1105,271 @@ window.__ModuleLoader__.load({
719
1105
  }, label)
720
1106
  }
721
1107
 
1108
+ function isInstalled(name) {
1109
+ const target = String(name || '')
1110
+ return plugins.some((plugin) => {
1111
+ const moduleName = String(plugin.moduleName || '')
1112
+ return moduleName === target || plugin.patchId === target || plugin.entryId === `include:${target}`
1113
+ })
1114
+ }
1115
+
1116
+ async function installFromMarket(name) {
1117
+ if (typeof props.installBundle !== 'function' || busyKey) return
1118
+ setBusyKey(name)
1119
+ try {
1120
+ const result = await props.installBundle(String(name))
1121
+ const value = result && result.ok === true ? result.value : result
1122
+ if (value && value.application === 'failed') {
1123
+ setHotError((value.error && (value.error.diagnostic || value.error.code)) || '')
1124
+ } else if (value && Array.isArray(value.pendingBuilds) && value.pendingBuilds.length) {
1125
+ setHotError(`需要批准构建脚本:${value.pendingBuilds.join(', ')}`)
1126
+ }
1127
+ await load()
1128
+ } catch (failure) {
1129
+ setHotError(failure instanceof Error ? failure.message : '')
1130
+ } finally {
1131
+ setBusyKey('')
1132
+ }
1133
+ }
1134
+
1135
+ async function removeFromMarket(name) {
1136
+ if (typeof props.removeBundle !== 'function' || busyKey) return
1137
+ setBusyKey(name)
1138
+ setConfirmKey('')
1139
+ try {
1140
+ const result = await props.removeBundle(String(name))
1141
+ const value = result && result.ok === true ? result.value : result
1142
+ if (value && value.application === 'failed') {
1143
+ setHotError((value.error && (value.error.diagnostic || value.error.code)) || '')
1144
+ }
1145
+ await load()
1146
+ } catch (failure) {
1147
+ setHotError(failure instanceof Error ? failure.message : '')
1148
+ } finally {
1149
+ setBusyKey('')
1150
+ }
1151
+ }
1152
+
1153
+ function hotAction(row) {
1154
+ const name = row.name
1155
+ if (busyKey === name) {
1156
+ return h('span', { className: 'dsh-pl-busy' }, isInstalled(name) ? textOf(t, 'list.removing') : textOf(t, 'list.installing'))
1157
+ }
1158
+ if (isInstalled(name)) {
1159
+ return confirmKey === name
1160
+ ? h('button', {
1161
+ type: 'button',
1162
+ className: 'dsh-pl-card-btn',
1163
+ onClick: (event) => { event.stopPropagation(); void removeFromMarket(name) },
1164
+ }, textOf(t, 'list.uninstallConfirm'))
1165
+ : h('button', {
1166
+ type: 'button',
1167
+ className: 'dsh-pl-card-btn',
1168
+ onClick: (event) => { event.stopPropagation(); setConfirmKey(name) },
1169
+ }, textOf(t, 'list.uninstall'))
1170
+ }
1171
+ return h('button', {
1172
+ type: 'button',
1173
+ className: 'dsh-pl-card-btn',
1174
+ onClick: (event) => { event.stopPropagation(); void installFromMarket(name) },
1175
+ }, textOf(t, 'list.install'))
1176
+ }
1177
+
1178
+ function hotView() {
1179
+ const rows = hot && Array.isArray(hot.top) ? hot.top : []
1180
+ const query = hotQuery.trim().toLowerCase()
1181
+ const shown = query
1182
+ ? rows.filter((row) => [row.name, row.summary, row.category]
1183
+ .filter(Boolean)
1184
+ .some((field) => String(field).toLowerCase().includes(query)))
1185
+ : rows
1186
+ return h('section', { className: 'dsh-pl-hot' },
1187
+ h('div', { className: 'dsh-pl-block-head' },
1188
+ h('strong', null, textOf(t, 'list.hotTitle')),
1189
+ hot && hot.fromDay
1190
+ ? h('span', null, rows.length === shown.length ? `${hot.fromDay} → ${hot.toDay}` : `${shown.length} / ${rows.length}`)
1191
+ : null),
1192
+ h('p', { className: 'dsh-pl-note' }, textOf(t, 'list.hotHint')),
1193
+ hotStatus === 'loading'
1194
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.hotLoading'))
1195
+ : hotStatus === 'error'
1196
+ ? h('p', { className: 'dsh-pl-state' }, hotError ? `${textOf(t, 'list.hotError')}(${hotError})` : textOf(t, 'list.hotError'))
1197
+ : rows.length === 0
1198
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.hotEmpty'))
1199
+ : shown.length === 0
1200
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.hotNoMatch'))
1201
+ : h('div', { className: `dsh-pl-grid${view === 'card' ? ' is-card' : ''}` },
1202
+ shown.map((row, index) => h(PluginCard, {
1203
+ key: row.repo || row.name || index,
1204
+ icon: row.icon ? h('img', { src: row.icon, alt: '', draggable: false }) : null,
1205
+ title: row.name,
1206
+ subtitle: row.summary || row.category || '',
1207
+ className: 'is-market',
1208
+ badge: h('span', { className: 'dsh-pl-delta' }, `+${row.delta.toLocaleString()}`),
1209
+ onClick: () => setSelectedMarket(row),
1210
+ actions: [hotAction(row)],
1211
+ }))))
1212
+ }
1213
+
1214
+ const searchTimer = React.useRef(null)
1215
+
1216
+ const runMarketSearch = React.useCallback((query) => {
1217
+ const call = props.searchCatalog
1218
+ if (typeof call !== 'function') return
1219
+ if (!query.trim()) {
1220
+ setSearchResult(null)
1221
+ setSearchStatus('')
1222
+ return
1223
+ }
1224
+ setSearchStatus('loading')
1225
+ Promise.resolve(call({ query })).then((result) => {
1226
+ if (result && result.ok === false) {
1227
+ setSearchResult(null)
1228
+ setSearchStatus('error')
1229
+ return
1230
+ }
1231
+ const value = result && result.ok === true ? result.value : result
1232
+ setSearchResult(value && Array.isArray(value.items) ? value.items : [])
1233
+ setSearchStatus('ready')
1234
+ }).catch(() => {
1235
+ setSearchResult(null)
1236
+ setSearchStatus('error')
1237
+ })
1238
+ }, [props.searchCatalog])
1239
+
1240
+ function onMarketQuery(value) {
1241
+ setMarketQuery(value)
1242
+ if (searchTimer.current) clearTimeout(searchTimer.current)
1243
+ searchTimer.current = setTimeout(() => runMarketSearch(value), 300)
1244
+ }
1245
+
1246
+ function marketDetail(row) {
1247
+ const installed = isInstalled(row.name)
1248
+ const facts = [
1249
+ typeof row.delta === 'number'
1250
+ ? { label: textOf(t, 'list.hotDelta'), value: `+${row.delta.toLocaleString()}` }
1251
+ : null,
1252
+ typeof row.stars === 'number'
1253
+ ? { label: textOf(t, 'list.hotStars'), value: row.stars.toLocaleString() }
1254
+ : null,
1255
+ row.version ? { label: textOf(t, 'list.version'), value: row.version } : null,
1256
+ row.category ? { label: textOf(t, 'list.hotCategory'), value: row.category } : null,
1257
+ row.repo ? { label: textOf(t, 'list.hotRepo'), value: row.repo } : null,
1258
+ { label: textOf(t, 'list.status'), value: installed ? textOf(t, 'list.installedState') : textOf(t, 'list.notInstalled') },
1259
+ ].filter(Boolean)
1260
+ return h(DetailPanel, {
1261
+ title: row.name,
1262
+ closeLabel: textOf(t, 'list.close'),
1263
+ onClose: () => setSelectedMarket(null),
1264
+ facts,
1265
+ note: row.summary || '',
1266
+ actions: [hotAction(row)],
1267
+ })
1268
+ }
1269
+
1270
+ function marketView() {
1271
+ const days = trend && Array.isArray(trend.days) ? trend.days : []
1272
+ const first = days[0]
1273
+ const last = days[days.length - 1]
1274
+ const span = first && last
1275
+ ? Math.max(1, Math.round((Date.parse(last.day) - Date.parse(first.day)) / 86400000))
1276
+ : 0
1277
+ const perDay = first && last ? Math.round((last.count - first.count) / Math.max(span, 1)) : 0
1278
+ const query = marketQuery.trim()
1279
+ const searchBox = h('input', {
1280
+ type: 'search',
1281
+ className: 'dsh-pl-search',
1282
+ placeholder: textOf(t, 'list.marketSearch'),
1283
+ value: marketQuery,
1284
+ onChange: (event) => onMarketQuery(event.target.value),
1285
+ })
1286
+ if (query) {
1287
+ const items = Array.isArray(searchResult) ? searchResult : []
1288
+ return h('div', { className: 'dsh-pl-market' },
1289
+ searchBox,
1290
+ searchStatus === 'loading'
1291
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketSearching'))
1292
+ : searchStatus === 'error'
1293
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.searchError'))
1294
+ : items.length === 0
1295
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketNoMatch'))
1296
+ : h('div', null,
1297
+ h('div', { className: 'dsh-pl-block-head' },
1298
+ h('strong', null, textOf(t, 'list.marketHits').replace('{n}', String(items.length)))),
1299
+ h('div', { className: `dsh-pl-grid${view === 'card' ? ' is-card' : ''}` },
1300
+ items.map((row, index) => h(PluginCard, {
1301
+ key: row.name || index,
1302
+ icon: null,
1303
+ title: row.name,
1304
+ subtitle: row.summary || row.category || '',
1305
+ className: 'is-market',
1306
+ onClick: () => setSelectedMarket(row),
1307
+ actions: [hotAction(row)],
1308
+ })))))
1309
+ }
1310
+ return h('div', { className: 'dsh-pl-market' },
1311
+ searchBox,
1312
+ trendStatus === 'loading'
1313
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketLoading'))
1314
+ : trendStatus === 'error'
1315
+ ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.marketError'))
1316
+ : days.length
1317
+ ? [
1318
+ h('section', { className: 'dsh-pl-block', key: 'trend' },
1319
+ h('div', { className: 'dsh-pl-block-head' },
1320
+ h('strong', null, textOf(t, 'list.chartTitle')),
1321
+ h('div', { className: 'dsh-pl-seg', role: 'group', 'aria-label': 'chart' },
1322
+ chip('cumulative', textOf(t, 'list.chartCumulative'), chartMode, setChartMode),
1323
+ chip('daily', textOf(t, 'list.chartDaily'), chartMode, setChartMode))),
1324
+ h('div', { className: 'dsh-pl-market-stats' },
1325
+ stat(textOf(t, 'list.countNow'), last.count.toLocaleString()),
1326
+ stat(textOf(t, 'list.countFiltered'), (last.rejected || 0).toLocaleString()),
1327
+ stat(textOf(t, 'list.perDay'), `+${perDay.toLocaleString()}`),
1328
+ stat(textOf(t, 'list.spanDays'), String(days.length))),
1329
+ h('div', { className: 'dsh-pl-chart-wrap' },
1330
+ chartMode === 'daily'
1331
+ ? barChart(days, hoverIndex, setHoverIndex)
1332
+ : trendChart(days, hoverIndex, setHoverIndex, textOf(t, 'list.marketFirstNote'))),
1333
+ h('p', { className: 'dsh-pl-note' }, chartMode === 'daily' ? textOf(t, 'list.chartDailyHint') : textOf(t, 'list.marketHint'))),
1334
+ hotView(),
1335
+ ]
1336
+ : null)
1337
+ }
1338
+
1339
+ function stat(label, value) {
1340
+ return h('div', { className: 'dsh-pl-stat' },
1341
+ h('span', null, label),
1342
+ h('strong', null, value))
1343
+ }
1344
+
722
1345
  return h('div', { className: 'dsh-pl' },
723
1346
  h('style', null, CSS),
724
1347
  h('header', { className: 'dsh-pl-bar' },
725
1348
  h('div', { className: 'dsh-pl-tools' },
726
1349
  h('div', { className: 'dsh-pl-seg', role: 'group', 'aria-label': 'filter' },
727
- chip('installed', textOf(t, 'list.installed'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
728
- chip('builtin', textOf(t, 'list.builtin'), filter, (id) => { setFilter(id); remember({ filter: id }) }),
729
- chip('all', textOf(t, 'list.all'), filter, (id) => { setFilter(id); remember({ filter: id }) })),
730
- h('button', { type: 'button', onClick: () => { void load() } }, textOf(t, 'list.refresh')),
1350
+ chip('installed', textOf(t, 'list.installed'), surface === 'market' ? '' : filter, (id) => { setSurface('plugins'); setFilter(id); remember({ filter: id }) }),
1351
+ chip('builtin', textOf(t, 'list.builtin'), surface === 'market' ? '' : filter, (id) => { setSurface('plugins'); setFilter(id); remember({ filter: id }) }),
1352
+ chip('market', textOf(t, 'list.market'), surface === 'market' ? 'market' : '', () => setSurface('market'))),
1353
+ h('button', {
1354
+ type: 'button',
1355
+ className: 'dsh-pl-refresh',
1356
+ onClick: () => {
1357
+ if (surface === 'market') {
1358
+ try { window.localStorage.removeItem('dsh.plugin-list.market.v1') } catch { /* ignore */ }
1359
+ setTrendStatus('')
1360
+ setHotStatus('')
1361
+ } else {
1362
+ void load()
1363
+ }
1364
+ },
1365
+ }, textOf(t, 'list.refresh')),
731
1366
  h('div', { className: 'dsh-pl-view', role: 'group', 'aria-label': 'view' },
732
1367
  chip('list', textOf(t, 'list.viewList'), view, (id) => { setView(id); remember({ layout: id }) }),
733
1368
  chip('card', textOf(t, 'list.viewCard'), view, (id) => { setView(id); remember({ layout: id }) })),
734
1369
  h('span', { className: 'dsh-pl-version' }, VERSION))),
735
- status === 'loading' && plugins.length === 0
1370
+ surface === 'market'
1371
+ ? selectedMarket ? marketDetail(selectedMarket) : marketView()
1372
+ : status === 'loading' && plugins.length === 0
736
1373
  ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.loading'))
737
1374
  : status === 'unavailable'
738
1375
  ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.unavailable'))
@@ -748,66 +1385,80 @@ window.__ModuleLoader__.load({
748
1385
  ? h('p', { className: 'dsh-pl-state' }, textOf(t, 'list.empty'))
749
1386
  : visible.map((plugin) => {
750
1387
  const title = pluginTitle(plugin, localeId)
751
- const icon = safeIcon(plugin.meta && plugin.meta.icon)
1388
+ const iconUrl = safeIcon(plugin.meta && plugin.meta.icon)
752
1389
  const kind = tabKindFor(plugin, guide, localeId)
753
1390
  const pinned = pins.includes(plugin.entryId)
754
- return h('div', {
1391
+ const places = placesFor(plugin, guide, seats, localeId)
1392
+ .map((place) => textOf(t, place === 'right' ? 'list.placeRight' : place === 'settings' ? 'list.placeSettings' : 'list.placeLeft'))
1393
+ .join(' · ') || textOf(t, 'list.placeNone')
1394
+ const classes = [
1395
+ selectedId === plugin.entryId ? 'is-on' : '',
1396
+ pinned ? 'is-pinned' : '',
1397
+ plugin.fiberPhase === 'failed' ? 'is-failed' : '',
1398
+ ].filter(Boolean).join(' ')
1399
+ return h(PluginCard, {
755
1400
  key: plugin.entryId,
756
- className: `dsh-pl-item${selectedId === plugin.entryId ? ' is-on' : ''}${pinned ? ' is-pinned' : ''}${plugin.fiberPhase === 'failed' ? ' is-failed' : ''}`,
757
- role: 'button',
758
- tabIndex: 0,
1401
+ icon: iconUrl ? h('img', { src: iconUrl, alt: '', draggable: false }) : null,
1402
+ title,
1403
+ subtitle: places,
1404
+ className: classes,
759
1405
  onClick: () => setSelectedId(plugin.entryId),
760
- onKeyDown: (event) => {
761
- if (event.key === 'Enter' || event.key === ' ') {
762
- event.preventDefault()
763
- setSelectedId(plugin.entryId)
764
- }
765
- },
766
- },
767
- icon
768
- ? h('img', { src: icon, alt: '', draggable: false })
769
- : h(DefaultPluginIcon),
770
- h('span', { className: 'dsh-pl-copy' },
771
- h('strong', null, title),
772
- h('span', { className: 'dsh-pl-where' },
773
- (placesFor(plugin, guide, seats, localeId).map((place) => textOf(t, place === 'right' ? 'list.placeRight' : place === 'settings' ? 'list.placeSettings' : 'list.placeLeft')).join(' · ')
774
- || textOf(t, 'list.placeNone')))),
775
- h('div', { className: 'dsh-pl-acts' },
776
- h('button', {
777
- type: 'button',
778
- className: 'dsh-pl-pin',
779
- 'aria-pressed': pinned,
780
- onClick: (event) => {
781
- event.stopPropagation()
782
- togglePin(plugin.entryId)
783
- },
784
- }, pinned ? textOf(t, 'list.unpin') : textOf(t, 'list.pin')),
785
- kind ? h('button', {
786
- type: 'button',
787
- className: 'dsh-pl-open',
788
- onClick: (event) => {
789
- event.stopPropagation()
790
- if (typeof openPanel === 'function') openPanel(kind)
791
- },
792
- }, textOf(t, 'list.open')) : null))
1406
+ actions: [
1407
+ h('button', {
1408
+ key: 'pin',
1409
+ type: 'button',
1410
+ className: 'dsh-pl-card-btn',
1411
+ 'aria-pressed': pinned,
1412
+ onClick: (event) => {
1413
+ event.stopPropagation()
1414
+ togglePin(plugin.entryId)
1415
+ },
1416
+ }, pinned ? textOf(t, 'list.unpin') : textOf(t, 'list.pin')),
1417
+ kind ? h('button', {
1418
+ key: 'open',
1419
+ type: 'button',
1420
+ className: 'dsh-pl-card-btn',
1421
+ onClick: (event) => {
1422
+ event.stopPropagation()
1423
+ if (typeof openPanel === 'function') openPanel(kind)
1424
+ },
1425
+ }, textOf(t, 'list.open')) : null,
1426
+ ],
1427
+ })
793
1428
  }))))
794
1429
  }
795
1430
 
796
1431
  const CSS = `
797
1432
  .dsh-pl, .dsh-pl * { box-sizing: border-box; }
798
- .dsh-pl { display: flex; flex-direction: column; flex: 1 1 auto; height: 100%; min-height: 0; min-width: 0; padding-top: 8px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); font: 13px/1.4 inherit; }
1433
+ .dsh-pl {
1434
+ --pl-fs-xs: 11px;
1435
+ --pl-fs-sm: 12px;
1436
+ --pl-fs-md: 13px;
1437
+ --pl-fs-lg: 14px;
1438
+ --pl-fs-title: 16px;
1439
+ --pl-fs-num: 18px;
1440
+ --pl-gap-1: 4px;
1441
+ --pl-gap-2: 8px;
1442
+ --pl-gap-3: 12px;
1443
+ --pl-radius-card: 16px;
1444
+ --pl-radius-inner: 12px;
1445
+ --pl-radius-chip: 10px;
1446
+ --pl-radius-pill: 7px;
1447
+ }
1448
+ .dsh-pl { display: flex; flex-direction: column; flex: 1 1 auto; height: 100%; min-height: 0; min-width: 0; padding-top: 8px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); font-family: inherit; font-size: var(--pl-fs-md); line-height: 1.4; }
799
1449
  .dsh-pl-bar { display: flex; flex: none; flex-direction: column; gap: 8px; margin: 0; padding: 12px 12px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-specific-sidebar-fill, var(--dsw-alias-bg-layer-1)); position: relative; z-index: 1; }
800
1450
  .dsh-pl-top { display: flex; align-items: center; justify-content: space-between; }
801
- .dsh-pl-version, .dsh-pl-hint, .dsh-pl-item span { color: var(--dsw-alias-label-secondary); font-size: 11px; }
1451
+ .dsh-pl-version, .dsh-pl-hint, .dsh-pl-item span { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-xs); }
802
1452
  .dsh-pl-hint { margin: 0; }
803
1453
  .dsh-pl-tools { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
804
- .dsh-pl-version { margin-left: 8px; }
805
- .dsh-pl-view { display: flex; gap: 2px; margin-left: auto; padding: 2px; border-radius: 8px; background: var(--dsw-alias-bg-layer-2); }
806
- .dsh-pl-view button { border: 0; border-radius: 6px; padding: 3px 8px; font-size: 11px; background: transparent; color: var(--dsw-alias-label-secondary); }
1454
+ .dsh-pl-refresh { margin-left: auto; }
1455
+ .dsh-pl-version { margin-left: 10px; }
1456
+ .dsh-pl-view { display: flex; gap: 2px; padding: 2px; border-radius: var(--pl-radius-inner); background: var(--dsw-alias-bg-layer-2); }
1457
+ .dsh-pl-view button { border: 0; border-radius: var(--pl-radius-pill); padding: 3px 8px; font-size: var(--pl-fs-xs); background: transparent; color: var(--dsw-alias-label-secondary); }
807
1458
  .dsh-pl-view button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; }
808
- .dsh-pl-seg { display: flex; gap: 2px; padding: 3px; border: 0; border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }
809
- .dsh-pl button { font: inherit; font-size: 12px; line-height: 1.2; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 6px 12px; cursor: pointer; }
810
- .dsh-pl-seg button { border: 0; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); padding: 5px 12px; }
1459
+ .dsh-pl-seg { display: flex; gap: 2px; padding: 3px; border: 0; border-radius: var(--pl-radius-inner); background: var(--dsw-alias-bg-layer-2); }
1460
+ .dsh-pl button { font: inherit; font-size: var(--pl-fs-sm); line-height: 1.2; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); border: 1px solid var(--dsw-alias-border-l1); border-radius: var(--pl-radius-chip); padding: 6px 12px; cursor: pointer; }
1461
+ .dsh-pl-seg button { border: 0; border-radius: var(--pl-radius-pill); background: transparent; color: var(--dsw-alias-label-secondary); padding: 5px 12px; }
811
1462
  .dsh-pl-seg button.is-on { background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-weight: 600; box-shadow: none; }
812
1463
  .dsh-pl-item.is-on { border-color: var(--dsw-alias-brand-primary); background: var(--dsw-alias-bg-layer-2); box-shadow: inset 0 0 0 1px var(--dsw-alias-brand-primary); }
813
1464
  .dsh-pl-body { display: flex; flex: 1 1 auto; min-height: 0; min-width: 0; overflow: hidden; }
@@ -819,46 +1470,67 @@ window.__ModuleLoader__.load({
819
1470
  .dsh-pl-acts { display: flex; gap: 6px; align-items: center; margin-left: auto; flex: none; }
820
1471
  .dsh-pl-item { position: relative; }
821
1472
  .dsh-pl-grid.is-card .dsh-pl-acts { position: absolute; top: 8px; right: 8px; margin: 0; padding: 0; background: transparent; opacity: 0; pointer-events: none; transition: opacity .12s ease; }
822
- .dsh-pl-grid.is-card .dsh-pl-acts .dsh-pl-pin, .dsh-pl-grid.is-card .dsh-pl-acts .dsh-pl-open { padding: 4px 8px; font-size: 11px; border: 0; background: var(--dsw-alias-bg-layer-2); }
1473
+ .dsh-pl-grid.is-card .dsh-pl-acts .dsh-pl-card-btn { padding: 4px 8px; font-size: var(--pl-fs-xs); border: 0; background: var(--dsw-alias-bg-layer-2); }
823
1474
  .dsh-pl-grid.is-card .dsh-pl-item:hover .dsh-pl-acts, .dsh-pl-grid.is-card .dsh-pl-item:focus-within .dsh-pl-acts { opacity: 1; pointer-events: auto; }
824
- .dsh-pl-item { display: flex; gap: 10px; align-items: center; width: 100%; min-width: 0; min-height: 64px; padding: 12px; text-align: left; border: 1px solid var(--dsw-alias-border-l2); border-radius: 16px; background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
825
- .dsh-pl-open, .dsh-pl-pin { margin-left: 0; flex: none; position: static; }
1475
+ .dsh-pl-item { display: flex; gap: 10px; align-items: center; width: 100%; min-width: 0; min-height: 64px; padding: 12px; text-align: left; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--pl-radius-card); background: var(--dsw-alias-bg-layer-1); overflow: hidden; }
1476
+ .dsh-pl-card-btn { margin-left: 0; flex: none; position: static; }
826
1477
  .dsh-pl-item.is-pinned { background: color-mix(in srgb, var(--dsw-alias-label-primary) 7%, var(--dsw-alias-bg-layer-1)); }
827
- .dsh-pl-item img, .dsh-pl-fallback { width: 22px; height: 22px; border-radius: 6px; object-fit: cover; flex: none; }
1478
+ .dsh-pl-item img, .dsh-pl-fallback { width: 22px; height: 22px; border-radius: var(--pl-radius-pill); object-fit: cover; flex: none; }
828
1479
  .dsh-pl-copy { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 2px; }
829
- .dsh-pl-copy strong { font-size: 14px; font-weight: 560; color: var(--dsw-alias-label-primary); white-space: normal; overflow-wrap: anywhere; }
830
- .dsh-pl-where { color: var(--dsw-alias-label-secondary); font-size: 11px; }
1480
+ .dsh-pl-copy strong { font-size: var(--pl-fs-lg); font-weight: 560; color: var(--dsw-alias-label-primary); white-space: normal; overflow-wrap: anywhere; }
1481
+ .dsh-pl-where { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-xs); }
1482
+ .dsh-pl-item.is-market .dsh-pl-where { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1483
+ .dsh-pl-item.is-market .dsh-pl-copy strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
831
1484
  .dsh-pl-item.is-failed { border-color: var(--dsw-alias-state-error-primary); }
832
1485
  .dsh-pl-state { margin: 16px; color: var(--dsw-alias-label-secondary); }
833
1486
  .dsh-pl-detail { flex: 1 1 auto; width: auto; max-width: none; min-width: 0; min-height: 0; overflow: auto; background: var(--dsw-alias-bg-base); padding: 12px 16px 20px; }
834
1487
  .dsh-pl-detail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
835
- .dsh-pl-detail-head strong { flex: 1 1 auto; min-width: 0; font-size: 16px; line-height: 1.35; overflow-wrap: anywhere; }
1488
+ .dsh-pl-detail-head strong { flex: 1 1 auto; min-width: 0; font-size: var(--pl-fs-title); line-height: 1.35; overflow-wrap: anywhere; }
836
1489
  .dsh-pl-detail-head button { flex: none; white-space: nowrap; align-self: flex-start; }
837
1490
  .dsh-pl-detail-head img, .dsh-pl-detail-head .dsh-pl-fallback { width: 32px; height: 32px; }
838
1491
  .dsh-pl-fact { display: grid; grid-template-columns: 4.5em minmax(0, 1fr); column-gap: 12px; align-items: start; margin: 0 0 10px; }
839
- .dsh-pl-fact-label { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.45; }
840
- .dsh-pl-fact-value { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
841
- .dsh-pl-detail h3 { margin: 14px 0 6px; font-size: 12px; color: var(--dsw-alias-label-secondary); }
842
- .dsh-pl-detail p, .dsh-pl-doc { margin: 0 0 8px; font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
843
- .dsh-pl-note { color: var(--dsw-alias-label-secondary); }
1492
+ .dsh-pl-fact-label { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-sm); line-height: 1.45; }
1493
+ .dsh-pl-fact-value { color: var(--dsw-alias-label-primary); font-size: var(--pl-fs-md); line-height: 1.45; overflow-wrap: anywhere; }
1494
+ .dsh-pl-detail h3 { margin: 14px 0 6px; font-size: var(--pl-fs-sm); color: var(--dsw-alias-label-secondary); }
1495
+ .dsh-pl-detail p, .dsh-pl-doc { margin: 0 0 8px; font-size: var(--pl-fs-md); line-height: 1.45; overflow-wrap: anywhere; }
1496
+ .dsh-pl-note { color: var(--dsw-alias-label-secondary); font-size: var(--pl-fs-sm); line-height: 1.5; }
844
1497
  .dsh-pl-doc-block { margin-top: 12px; }
845
- .dsh-pl-md h3, .dsh-pl-md-h { margin: 14px 0 6px; font-size: 14px; line-height: 1.4; font-weight: 650; color: var(--dsw-alias-label-primary); }
846
- .dsh-pl-md h4 { margin: 10px 0 4px; font-size: 13px; font-weight: 650; }
847
- .dsh-pl-md p, .dsh-pl-md li { margin: 0 0 8px; font-size: 13px; line-height: 1.55; }
1498
+ .dsh-pl-md h3, .dsh-pl-md-h { margin: 14px 0 6px; font-size: var(--pl-fs-lg); line-height: 1.4; font-weight: 650; color: var(--dsw-alias-label-primary); }
1499
+ .dsh-pl-md h4 { margin: 10px 0 4px; font-size: var(--pl-fs-md); font-weight: 650; }
1500
+ .dsh-pl-md p, .dsh-pl-md li { margin: 0 0 8px; font-size: var(--pl-fs-md); line-height: 1.55; }
848
1501
  .dsh-pl-md ul { margin: 0 0 10px; padding-left: 18px; }
849
- .dsh-pl-md code { font-size: 12px; padding: 0 4px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); }
1502
+ .dsh-pl-md code { font-size: var(--pl-fs-sm); padding: 0 4px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2); }
850
1503
  .dsh-pl-md a { color: var(--dsw-alias-brand-primary); }
851
- .dsh-pl-table-wrap { margin: 8px 0 14px; overflow-x: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; }
852
- .dsh-pl-md table { width: 100%; border-collapse: collapse; font-size: 13px; }
1504
+ .dsh-pl-table-wrap { margin: 8px 0 14px; overflow-x: auto; border: 1px solid var(--dsw-alias-border-l1); border-radius: var(--pl-radius-chip); }
1505
+ .dsh-pl-md table { width: 100%; border-collapse: collapse; font-size: var(--pl-fs-md); }
853
1506
  .dsh-pl-md th, .dsh-pl-md td { padding: 8px 10px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--dsw-alias-border-l1); }
854
1507
  .dsh-pl-md th { color: var(--dsw-alias-label-secondary); font-weight: 650; background: var(--dsw-alias-bg-layer-1); white-space: nowrap; }
855
1508
  .dsh-pl-md td:first-child { white-space: nowrap; }
856
1509
  .dsh-pl-md tr:last-child td { border-bottom: 0; }
1510
+ .dsh-pl-market { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 12px; }
1511
+ .dsh-pl-market-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-bottom: 12px; }
1512
+ .dsh-pl-stat { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--pl-radius-card); background: var(--dsw-alias-bg-layer-1); }
1513
+ .dsh-pl-stat span { font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
1514
+ .dsh-pl-stat strong { font-size: var(--pl-fs-num); font-weight: 600; line-height: 1.2; }
1515
+ .dsh-pl-chart-wrap { padding: 10px 12px 4px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--pl-radius-card); background: var(--dsw-alias-bg-layer-1); }
1516
+ .dsh-pl-chart { display: block; width: 100%; height: auto; }
1517
+ .dsh-pl-block-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
1518
+ .dsh-pl-block-head strong { font-size: var(--pl-fs-lg); }
1519
+ .dsh-pl-block-head span { font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
1520
+ .dsh-pl-block { margin-bottom: 22px; }
1521
+ .dsh-pl-market .dsh-pl-note { margin: 10px 2px 0; }
1522
+ .dsh-pl-hot { margin-top: 4px; }
1523
+ .dsh-pl-search { width: 100%; margin: 2px 0 8px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: var(--pl-radius-inner); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: inherit; font-size: var(--pl-fs-sm); }
1524
+ .dsh-pl-search::placeholder { color: var(--dsw-alias-label-secondary); }
1525
+ .dsh-pl-search:focus { outline: none; border-color: var(--dsw-alias-brand-primary); }
1526
+ .dsh-pl-delta { flex: none; margin-left: auto; font-size: var(--pl-fs-sm); font-weight: 600; color: var(--dsw-alias-brand-primary); }
1527
+ .dsh-pl-busy { flex: none; font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
1528
+ .dsh-pl-hot-busy { flex: none; font-size: var(--pl-fs-xs); color: var(--dsw-alias-label-secondary); }
857
1529
  .dsh-pl-actions { display: flex; gap: 8px; align-items: center; margin-top: 16px; }
858
1530
  `
859
1531
 
860
1532
  return {
861
- name: 'dsh-plugins-ui',
1533
+ name: 'dsh-plugin-panel',
862
1534
  inject: ['slots', 'sidebarRightTabs', 'sidebarRight', 'layout', 'locale', 'remote', 'remote.pluginManager', 'remote.session', 'connection'],
863
1535
  apply(ctx) {
864
1536
  ctx.effect(() => ctx.locale.register(NS, COPY), 'plugin-list: dictionaries')
@@ -870,6 +1542,11 @@ window.__ModuleLoader__.load({
870
1542
  const setPluginEnabled = pluginManager.setPluginEnabled.bind(pluginManager)
871
1543
  const rpcCall = ctx.connection.rpc.call.bind(ctx.connection.rpc)
872
1544
  const loadFacts = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'facts', payload || {})
1545
+ const loadMarketTrend = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'trend', payload || {})
1546
+ const loadMarketHot = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'hot', payload || {})
1547
+ const searchCatalog = (payload) => rpcCall('/dsh-plugins-ui-rpc', 'search', payload || {})
1548
+ const installBundle = pluginManager.installBundle.bind(pluginManager)
1549
+ const removeBundle = pluginManager.removeBundle.bind(pluginManager)
873
1550
  const revealPackage = async (payload) => {
874
1551
  const input = payload && typeof payload === 'object' ? payload : {}
875
1552
  const dir = directoryFromSource(input.source)
@@ -953,7 +1630,7 @@ window.__ModuleLoader__.load({
953
1630
  name: 'sidebar.right.pane.tab',
954
1631
  key: PANEL_ID,
955
1632
  locale: NS,
956
- inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, loadFacts, revealPackage, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
1633
+ inject: () => ({ listPlugins, listBundles, readBundleDetail, setPluginEnabled, installBundle, removeBundle, loadFacts, revealPackage, loadMarketTrend, loadMarketHot, searchCatalog, listGuide, subscribeGuide, listSeats, subscribeSeats, openPanel, t, readLocale, subscribeLocale }),
957
1634
  }, ListView)), 'plugin-list: tab body')
958
1635
  },
959
1636
  }