dsh-plugin 1.1.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/client/client.js +4465 -1442
  2. package/client/client.js.map +1 -1
  3. package/lib/http/routes.js +438 -22
  4. package/lib/index.js +1 -1
  5. package/lib/services/install/install.js +4 -0
  6. package/lib/services/install/npm-check.js +55 -0
  7. package/lib/services/{npm-resolve.js → install/npm-resolve.js} +7 -5
  8. package/lib/services/{preflight.js → install/preflight.js} +15 -4
  9. package/lib/services/{task-queue.js → install/task-queue.js} +155 -66
  10. package/lib/services/loader.js +56 -1
  11. package/lib/services/log.js +138 -0
  12. package/lib/services/probe.js +157 -0
  13. package/lib/services/{profile.js → profile/profile.js} +29 -1
  14. package/lib/services/settings.js +60 -0
  15. package/lib/types/http/routes.d.ts +1 -1
  16. package/lib/types/index.d.ts +1 -1
  17. package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +3 -0
  18. package/lib/types/services/{install.d.ts → install/install.d.ts} +5 -5
  19. package/lib/types/services/install/npm-check.d.ts +12 -0
  20. package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
  21. package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
  22. package/lib/types/services/loader.d.ts +14 -8
  23. package/lib/types/services/log.d.ts +49 -0
  24. package/lib/types/services/probe.d.ts +30 -0
  25. package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +1 -1
  26. package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +2 -0
  27. package/lib/types/services/settings.d.ts +24 -0
  28. package/package.json +1 -1
  29. package/src/client/components/PluginHubSection.tsx +341 -120
  30. package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +11 -7
  31. package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +5 -4
  32. package/src/client/components/layout/CatalogControls.tsx +80 -0
  33. package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +4 -4
  34. package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +3 -3
  35. package/src/client/components/layout/SectionTabs.tsx +60 -0
  36. package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +7 -7
  37. package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +5 -5
  38. package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
  39. package/src/client/components/modals/LogsModal.tsx +301 -0
  40. package/src/client/components/modals/LogsPathModal.tsx +161 -0
  41. package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +47 -20
  42. package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +2 -2
  43. package/src/client/components/{modals.tsx → modals/modals.tsx} +117 -40
  44. package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +14 -6
  45. package/src/client/components/ui/Toggle.tsx +28 -0
  46. package/src/client/components/ui/icons.tsx +343 -0
  47. package/src/client/components/views/CustomInstallView.tsx +250 -0
  48. package/src/client/components/views/DiagnosticsView.tsx +172 -0
  49. package/src/client/components/views/InstalledView.tsx +328 -0
  50. package/src/client/components/views/LogsView.tsx +150 -0
  51. package/src/client/components/views/MarketView.tsx +66 -0
  52. package/src/client/components/views/SettingsView.tsx +273 -0
  53. package/src/client/data/catalog.ts +38 -0
  54. package/src/client/data/host.ts +107 -0
  55. package/src/client/data/hub.ts +41 -0
  56. package/src/client/hooks/useCatalog.ts +71 -115
  57. package/src/client/hooks/useIncrementalList.ts +46 -0
  58. package/src/client/hooks/useLanguage.ts +40 -0
  59. package/src/client/hooks/useSettings.ts +90 -0
  60. package/src/client/hooks/useTaskQueue.ts +156 -39
  61. package/src/client/locales.ts +358 -38
  62. package/src/client/logic/constants.ts +86 -0
  63. package/src/client/{lib → logic}/failures.ts +95 -74
  64. package/src/client/logic/install-command.ts +56 -0
  65. package/src/client/logic/installed.ts +207 -0
  66. package/src/client/logic/normalize.ts +52 -0
  67. package/src/client/logic/urls.ts +125 -0
  68. package/src/client/styles/CustomInstallView.module.css +191 -0
  69. package/src/client/styles/DiagnosticsView.module.css +231 -0
  70. package/src/client/styles/Dropdown.module.css +42 -41
  71. package/src/client/styles/Header.module.css +100 -132
  72. package/src/client/styles/InstalledView.module.css +447 -0
  73. package/src/client/styles/List.module.css +6 -0
  74. package/src/client/styles/LogsView.module.css +503 -0
  75. package/src/client/styles/Modal.module.css +352 -70
  76. package/src/client/styles/SectionTabs.module.css +142 -0
  77. package/src/client/styles/SettingsView.module.css +232 -0
  78. package/src/client/styles/Toggle.module.css +60 -0
  79. package/src/client/styles/tokens.module.css +1 -0
  80. package/src/client/types.ts +23 -3
  81. package/src/server/http/routes.ts +414 -22
  82. package/src/server/index.ts +1 -1
  83. package/src/server/services/{install-types.ts → install/install-types.ts} +3 -1
  84. package/src/server/services/{install.ts → install/install.ts} +5 -5
  85. package/src/server/services/install/npm-check.ts +53 -0
  86. package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +7 -5
  87. package/src/server/services/{preflight.ts → install/preflight.ts} +16 -4
  88. package/src/server/services/{task-queue.ts → install/task-queue.ts} +157 -63
  89. package/src/server/services/loader.ts +55 -1
  90. package/src/server/services/log.ts +167 -0
  91. package/src/server/services/probe.ts +157 -0
  92. package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +1 -1
  93. package/src/server/services/{profile.ts → profile/profile.ts} +31 -1
  94. package/src/server/services/settings.ts +79 -0
  95. package/lib/services/install.js +0 -4
  96. package/src/client/components/CatalogControls.tsx +0 -108
  97. package/src/client/components/icons.tsx +0 -129
  98. package/src/client/lib/catalog.ts +0 -293
  99. package/src/client/lib/env.ts +0 -20
  100. /package/lib/services/{install-types.js → install/install-types.js} +0 -0
  101. /package/lib/services/{installed-versions.js → profile/installed-versions.js} +0 -0
  102. /package/lib/services/{pending-restart.js → profile/pending-restart.js} +0 -0
  103. /package/lib/services/{progress.js → profile/progress.js} +0 -0
  104. /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
  105. /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
  106. /package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +0 -0
  107. /package/src/client/{lib → logic}/format.ts +0 -0
  108. /package/src/client/{lib → logic}/renderMarkdown.ts +0 -0
  109. /package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +0 -0
  110. /package/src/server/services/{progress.ts → profile/progress.ts} +0 -0
@@ -10,12 +10,13 @@
10
10
  */
11
11
  import { createElement as h } from 'react'
12
12
  import type { MouseEvent } from 'react'
13
- import styles from '../styles/Modal.module.css'
14
- import type { EnvInfo, HubPlugin, TaskState, ToastState, Translate } from '../types.ts'
15
- import { CloseIcon, CopyIcon, LinkIcon } from './icons.tsx'
13
+ import styles from '../../styles/Modal.module.css'
14
+ import type { EnvInfo, HubPlugin, TaskState, ToastState, Translate } from '../../types.ts'
15
+ import { CloseIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
16
16
  import { ProgressView } from './ProgressView.tsx'
17
- import { installCommandOf, pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../lib/catalog.ts'
18
- import { classifyFailure } from '../lib/failures.ts'
17
+ import { installCommandOf } from '../../logic/install-command.ts'
18
+ import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
19
+ import { classifyFailure, npmTooLowVersion } from '../../logic/failures.ts'
19
20
 
20
21
  /** 完成结果视图:绿色对勾 + 标题/描述;
21
22
  * needsRestart=true(插件需重启才生效)→ 「稍后重启 / 立即重启」按钮对,点稍后重启后
@@ -56,7 +57,7 @@ function ResultView({
56
57
  h('div', { className: styles.resultRestarting }, restarting ? t('restarting') : t('restartHint')),
57
58
  h('div', { className: styles.modalActions },
58
59
  h('button', { className: styles.restartLater, onClick: onClose, disabled: restarting }, t('restartLater')),
59
- h('button', { className: styles.restartNow, onClick: onRestart, disabled: restarting },
60
+ h('button', { className: styles.restartNowWarning, onClick: onRestart, disabled: restarting },
60
61
  restarting ? t('restarting') : t('restartNow')),
61
62
  ),
62
63
  ])
@@ -66,7 +67,13 @@ function ResultView({
66
67
  }
67
68
 
68
69
  export interface InstallModalProps {
69
- plugin: HubPlugin
70
+ /** 目录插件(有完整元数据);命令行安装(custom)传 null 走 customTarget 模式 */
71
+ plugin: HubPlugin | null
72
+ /** 命令行安装目标(无目录元数据):提供时按裸目标展示确认/进度/结果,命令条显示 pnpm add <target> */
73
+ customTarget?: string
74
+ /** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):提供时按包列表展示确认/进度/结果,
75
+ * 命令条显示 npm install -g <pkgs> —— 装的是系统级 CLI 工具,不进插件列表、无需重启 */
76
+ globalNpm?: string[]
70
77
  done: boolean
71
78
  task: TaskState | null
72
79
  t: Translate
@@ -89,12 +96,14 @@ export interface InstallModalProps {
89
96
  /**
90
97
  * 信任确认弹窗:安装进入后台队列后弹窗仍可关闭(任务继续),
91
98
  * 只在本任务执行中展示实时进度;完成后切换为结果视图,与卸载一致。
99
+ * 目录插件与命令行安装(customTarget 模式)共用同一套确认/进度/结果流程。
92
100
  */
93
101
  export function InstallModal(props: InstallModalProps) {
94
- const { plugin, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, onClose, onCopy, onInstall, onRestart } = props
102
+ const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, onClose, onCopy, onInstall, onRestart } = props
95
103
  const busy = submitting || (task !== null && (task.status === 'pending' || task.status === 'running'))
96
- // 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分
97
- const name = plugin.displayName ?? plugin.slug
104
+ // 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分;
105
+ // 命令行安装无插件元数据 → 直接用输入的目标展示;全局 npm 安装 → 包列表空格拼接
106
+ const name = customTarget ?? (globalNpm !== undefined && globalNpm.length > 0 ? globalNpm.join(' ') : undefined) ?? plugin?.displayName ?? plugin?.slug ?? ''
98
107
  const busyTitle = (label: string) => langPath === 'zh/' ? `${name} 插件${label}` : `${label} ${name}`
99
108
  const title = busy
100
109
  ? task && task.status === 'pending'
@@ -138,10 +147,9 @@ export function InstallModal(props: InstallModalProps) {
138
147
  h('div', { className: styles.trustHint }, t('confirmDesc')),
139
148
  h('div', { className: styles.modalRow },
140
149
  h('span', { className: styles.modalLabel }, t('confirmPlugin')),
141
- h('span', { className: styles.modalValue, title: plugin.displayName ?? plugin.slug },
142
- plugin.displayName ?? plugin.slug),
150
+ h('span', { className: styles.modalValue, title: name }, name),
143
151
  ),
144
- plugin.source?.repo ? h('div', { className: styles.modalRow },
152
+ plugin?.source?.repo ? h('div', { className: styles.modalRow },
145
153
  h('span', { className: styles.modalLabel }, t('confirmSource')),
146
154
  // 来源仓库:可点击跳转官网收录详情页(新窗口打开)
147
155
  h('a', {
@@ -152,7 +160,8 @@ export function InstallModal(props: InstallModalProps) {
152
160
  title: plugin.source.repo,
153
161
  }, h(LinkIcon), plugin.source.repo),
154
162
  ) : null,
155
- // 手动命令条:整条点击即复制,右侧再放一个显式复制按钮
163
+ // 手动命令条:整条点击即复制,右侧再放一个显式复制按钮;
164
+ // 命令行安装(无目录数据)直接展示 pnpm add <target>;全局 npm 安装展示 npm install -g <pkgs>
156
165
  h('div', {
157
166
  className: styles.modalCmd,
158
167
  onClick: onCopy,
@@ -160,9 +169,16 @@ export function InstallModal(props: InstallModalProps) {
160
169
  role: 'button',
161
170
  tabIndex: 0,
162
171
  },
163
- h('span', { className: styles.modalCmdText }, installCommandOf(plugin)),
172
+ h('span', { className: styles.modalCmdText },
173
+ globalNpm !== undefined && globalNpm.length > 0
174
+ ? `npm install -g ${globalNpm.join(' ')}`
175
+ : customTarget ? `pnpm add ${customTarget}` : installCommandOf(plugin!)),
164
176
  h('span', { className: styles.modalCmdCopy }, h(CopyIcon), t('copyCmdLabel')),
165
177
  ),
178
+ // 全局 npm 安装说明:装的是系统级 CLI 工具,不进插件列表、无需重启宿主
179
+ globalNpm !== undefined && globalNpm.length > 0
180
+ ? h('div', { className: styles.cliOnlyHint }, t('globalNpmHint'))
181
+ : null,
166
182
  // AI 识别可能需要命令行辅助的插件(webInstallable=false):提示但不拦截,仍允许尝试一键安装
167
183
  cliOnly ? h('div', { className: styles.cliOnlyHint }, t('cliOnlyHint')) : null,
168
184
  task && task.status === 'pending' ? h('div', { className: styles.queuedHint }, t('queuedHint')) : null,
@@ -272,21 +288,22 @@ export function UninstallModal(props: UninstallModalProps) {
272
288
  task && task.status === 'pending' ? h('div', { className: styles.queuedHint }, t('queuedHint')) : null,
273
289
  task ? h(ProgressView, { task }) : null,
274
290
  h('div', { className: styles.modalActions },
275
- h('button', {
276
- className: styles.modalCancel,
277
- onClick: onCancel,
278
- }, t('confirmCancel')),
279
291
  // 复制卸载命令:万一直接卸载失败,可去终端手动执行
280
292
  h('button', {
281
293
  className: styles.modalCopy,
282
294
  disabled: busy,
283
295
  onClick: onCopyCommand,
284
296
  }, t('copyUninstallCommand')),
297
+ // 确认框语义:取消动作需明确指向「卸载」,避免歧义
298
+ h('button', {
299
+ className: styles.modalCancel,
300
+ onClick: onCancel,
301
+ }, t('cancelUninstall')),
285
302
  h('button', {
286
303
  className: styles.uninstallConfirm,
287
304
  disabled: busy,
288
305
  onClick: onConfirm,
289
- }, busy ? (task && task.status === 'pending' ? t('queuedUninstallTitle') : t('uninstalling')) : t('uninstall')),
306
+ }, busy ? (task && task.status === 'pending' ? t('queuedUninstallTitle') : t('uninstalling')) : t('uninstallConfirm')),
290
307
  ),
291
308
  ),
292
309
  ),
@@ -309,6 +326,16 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
309
326
  }) {
310
327
  // 失败归类:当前安装通道(npm/git)装不上 = 插件分发/依赖的问题,一律引导提 Issue
311
328
  const failureKind = classifyFailure(message)
329
+ // 服务端 [npm-too-low] 标记里带的本机 npm 版本(无标记为 null,提示文案降级为「怀疑」)
330
+ const npmVersion = npmTooLowVersion(message)
331
+ // 复制完整错误:实际执行/尝试过的安装命令(如有)+ 完整错误正文。
332
+ // 只复制 message 会丢上下文 —— 命令行安装输入的目标、执行过的命令都在 command/attempts 里,
333
+ // 光拷一句「unsupported install target」出去没人看得懂
334
+ const copyText = [
335
+ ...(command ? [command] : []),
336
+ ...(attempts ?? []),
337
+ message,
338
+ ].join('\n')
312
339
  return h('div', {
313
340
  className: styles.overlay,
314
341
  onClick: (e: MouseEvent<HTMLDivElement>) => {
@@ -341,33 +368,37 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
341
368
  title: repo,
342
369
  }, repo) : null,
343
370
  // 复制按钮放最上面、浅灰隐蔽;报错正文不允许鼠标选择复制,只能点这里
344
- h('button', { className: styles.errorCopySoft, onClick: () => onCopy(message) }, t('errorCopy')),
371
+ h('button', { className: styles.errorCopySoft, onClick: () => onCopy(copyText) }, t('errorCopy')),
345
372
  ),
346
373
  // 报错信息完整展示(可滚动),比失败记录的预览看得更多
347
374
  h('pre', { className: styles.errorBox }, message),
348
- // 插件问题(prepare 构建失败 / 原生依赖构建被拦截 / git 分发缺产物):都是插件打包分发问题 —— 先说明原因,按钮在下方引导提 Issue
349
- failureKind === 'pluginPrepare' || failureKind === 'pnpmIgnoredBuild'
350
- ? h('div', null, [
351
- // [packaging](预检/装后校验拦截:git 分发缺产物)与
352
- // prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
353
- h('div', { className: styles.failPrepareHint }, failureKind === 'pnpmIgnoredBuild'
354
- ? t('failIgnoredBuild')
355
- : /\[packaging\]/i.test(message) ? t('failPackagingHint') : t('failPrepareHint')),
356
- repo ? h('a', {
375
+ // npm 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题 ——
376
+ // 直接给升级指引,不引导提 Issue(提了也是 npm 的问题,插件作者无法修复)
377
+ failureKind === 'npmTooOld'
378
+ ? h('div', { className: styles.failPrepareHint },
379
+ t(npmVersion ? 'failNpmTooLowV' : 'failNpmTooLow', npmVersion ? { v: npmVersion } : undefined))
380
+ : failureKind === 'pluginPrepare' || failureKind === 'pnpmIgnoredBuild'
381
+ ? h('div', null, [
382
+ // [packaging](预检/装后校验拦截:git 分发缺产物)与
383
+ // prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
384
+ h('div', { className: styles.failPrepareHint }, failureKind === 'pnpmIgnoredBuild'
385
+ ? t('failIgnoredBuild')
386
+ : /\[packaging\]/i.test(message) ? t('failPackagingHint') : t('failPrepareHint')),
387
+ repo ? h('a', {
388
+ className: styles.failBigIssue,
389
+ href: pluginIssueUrl(repo, message, env, command, attempts),
390
+ target: '_blank',
391
+ rel: 'noopener noreferrer',
392
+ title: t('failIssueHint'),
393
+ }, t('failIssueBig')) : null,
394
+ ])
395
+ : repo ? h('a', {
357
396
  className: styles.failBigIssue,
358
397
  href: pluginIssueUrl(repo, message, env, command, attempts),
359
398
  target: '_blank',
360
399
  rel: 'noopener noreferrer',
361
400
  title: t('failIssueHint'),
362
401
  }, t('failIssueBig')) : null,
363
- ])
364
- : repo ? h('a', {
365
- className: styles.failBigIssue,
366
- href: pluginIssueUrl(repo, message, env, command, attempts),
367
- target: '_blank',
368
- rel: 'noopener noreferrer',
369
- title: t('failIssueHint'),
370
- }, t('failIssueBig')) : null,
371
402
  ),
372
403
  h('div', { className: styles.modalActions },
373
404
  h('button', { className: styles.restartLater, onClick: onClose }, t('errorClose')),
@@ -384,10 +415,56 @@ export function Toast({ toast, t }: { toast: ToastState; t: Translate }) {
384
415
  : toast.kind === 'done' ? t('installDone')
385
416
  : toast.kind === 'fail' ? t('installFail')
386
417
  : toast.kind === 'removed' ? t('uninstallDone')
387
- : t('uninstallFail')
388
- const fail = toast.kind === 'fail' || toast.kind === 'removeFail'
418
+ : toast.kind === 'revealFail' ? t('openFolderFail')
419
+ : toast.kind === 'updates' ? t('updatesFound', { n: toast.n ?? 0 })
420
+ : t('uninstallFail')
421
+ const fail = toast.kind === 'fail' || toast.kind === 'removeFail' || toast.kind === 'revealFail'
389
422
  return h('div', {
390
423
  key: toast.id,
391
424
  className: fail ? `${styles.toast} ${styles.toastFail}` : styles.toast,
392
425
  }, text)
393
426
  }
427
+
428
+ /** 待重启确认弹窗:已安装列表行内「重启」按钮点击后弹出,
429
+ * 与通知中心待重启条目同一交互(说明 + 稍后重启 / 立即重启),
430
+ * 避免行内按钮误触直接触发宿主重启。 */
431
+ export function RestartConfirmModal({ t, restarting, onClose, onRestartNow }: {
432
+ t: Translate
433
+ restarting: boolean
434
+ onClose: () => void
435
+ onRestartNow: () => void
436
+ }) {
437
+ return h('div', {
438
+ className: styles.overlay,
439
+ onClick: (e: MouseEvent<HTMLDivElement>) => { if (e.target === e.currentTarget) onClose() },
440
+ },
441
+ h('div', { className: styles.modal, role: 'dialog', 'aria-modal': 'true' },
442
+ h('div', { className: styles.modalHead },
443
+ h('div', { className: styles.modalTitle }, t('sectionPendingRestart')),
444
+ h('button', {
445
+ className: styles.modalClose,
446
+ type: 'button',
447
+ 'aria-label': t('errorClose'),
448
+ onClick: onClose,
449
+ }, h(CloseIcon)),
450
+ ),
451
+ h('div', { className: styles.modalBody },
452
+ h('div', { className: styles.failPrepareHint }, t('restartHint')),
453
+ h('div', { className: styles.modalActions },
454
+ h('button', {
455
+ className: styles.restartLater,
456
+ type: 'button',
457
+ disabled: restarting,
458
+ onClick: onClose,
459
+ }, t('restartLater')),
460
+ h('button', {
461
+ className: styles.restartNowWarning,
462
+ type: 'button',
463
+ disabled: restarting,
464
+ onClick: onRestartNow,
465
+ }, restarting ? t('restarting') : t('restartNow')),
466
+ ),
467
+ ),
468
+ ),
469
+ )
470
+ }
@@ -3,14 +3,17 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Custom dropdown (native <select> replacement).
6
+ * Button-style dropdown (native <select> replacement).
7
7
  *
8
- * macOS/浏览器原生下拉无法跟随宿主主题,这里自绘:触发器按钮 + 绝对定位面板,
9
- * 点外部/Esc 关闭;选项 hover 加重底色、激活项深色实心高亮,全部走 --dsw-alias-* 主题变量。
8
+ * The trigger is a compact toolbar button — hairline border + subtle grey
9
+ * fill + current value + chevron — so it sits naturally next to the other
10
+ * toolbar buttons and takes a single slot of horizontal space. Clicking
11
+ * outside or pressing Esc closes the panel; options get a hover fill and
12
+ * the active one a brand highlight, all driven by the --hub-* tokens.
10
13
  */
11
14
  import { createElement as h, useEffect, useRef, useState } from 'react'
12
15
  import type { JSX } from 'react'
13
- import styles from '../styles/Dropdown.module.css'
16
+ import styles from '../../styles/Dropdown.module.css'
14
17
  import { ChevronDownIcon } from './icons.tsx'
15
18
 
16
19
  export interface DropdownOption<T extends string> {
@@ -25,9 +28,11 @@ interface DropdownProps<T extends string> {
25
28
  options: DropdownOption<T>[]
26
29
  onChange: (value: T) => void
27
30
  title?: string
31
+ /** 额外挂到根节点上的类名(如 margin 微调) */
32
+ className?: string
28
33
  }
29
34
 
30
- export function Dropdown<T extends string>({ value, options, onChange, title }: DropdownProps<T>): JSX.Element {
35
+ export function Dropdown<T extends string>({ value, options, onChange, title, className }: DropdownProps<T>): JSX.Element {
31
36
  const [open, setOpen] = useState(false)
32
37
  const rootRef = useRef<HTMLDivElement | null>(null)
33
38
 
@@ -50,7 +55,10 @@ export function Dropdown<T extends string>({ value, options, onChange, title }:
50
55
 
51
56
  const current = options.find((o) => o.value === value)
52
57
 
53
- return h('div', { className: styles.dropdown, ref: rootRef },
58
+ return h('div', {
59
+ className: className ? `${styles.dropdown} ${className}` : styles.dropdown,
60
+ ref: rootRef,
61
+ },
54
62
  h('button', {
55
63
  className: styles.dropdownBtn,
56
64
  type: 'button',
@@ -0,0 +1,28 @@
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Compact switch used by the settings rows. Native-button based so it is
7
+ * keyboard focusable and announces state via aria-checked; styling driven
8
+ * by the --hub-* tokens (on = brand fill, off = neutral track).
9
+ */
10
+ import { createElement as h } from 'react'
11
+ import styles from '../../styles/Toggle.module.css'
12
+
13
+ export function Toggle({ checked, onChange, title, disabled }: {
14
+ checked: boolean
15
+ onChange: (next: boolean) => void
16
+ title?: string
17
+ disabled?: boolean
18
+ }) {
19
+ return h('button', {
20
+ type: 'button',
21
+ role: 'switch',
22
+ 'aria-checked': checked,
23
+ disabled,
24
+ title,
25
+ className: checked ? styles.toggleOn : styles.toggle,
26
+ onClick: () => onChange(!checked),
27
+ }, h('span', { className: styles.knob }))
28
+ }