refine(plugin-manager): polish install dialog guide, input, and wizard states
This commit is contained in:
1 parent
c8b17dd4d0
commit
bd6447479d
5 files changed
+86
-54
No files matched your search
@@ -397,7 +397,7 @@
|
||||
/* Wider than the default dialog: pnpm's lines are long, and the terminal keeps them unwrapped. The card never
|
||||
outgrows the viewport; the running screens scroll inside it so the head's controls stay reachable. */
|
||||
.installDialog {
|
||||
width: min(540px, 100%);
|
||||
width: min(560px, 100%);
|
||||
max-height: calc(100vh - 48px);
|
||||
}
|
||||
|
||||
@@ -425,16 +425,22 @@
|
||||
}
|
||||
|
||||
.installField input[type='text'] {
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
height: 40px;
|
||||
padding: 0 14px;
|
||||
border: 0.5px solid var(--dsw-alias-border-l4);
|
||||
border-radius: 10px;
|
||||
border-radius: 14px;
|
||||
outline: none;
|
||||
background: var(--dsw-alias-bg-layer-3);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.installField input[type='text']:focus {
|
||||
border-color: var(--dsw-alias-brand-primary);
|
||||
box-shadow: inset 0 0 0 0.5px var(--dsw-alias-brand-primary);
|
||||
}
|
||||
|
||||
.installField input[aria-invalid='true'] {
|
||||
border-color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
@@ -445,6 +451,7 @@
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
@@ -470,13 +477,12 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
padding: 8px 14px 14px;
|
||||
border: 0.5px solid var(--dsw-alias-border-l4);
|
||||
border-radius: 12px;
|
||||
border-radius: 14px;
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
}
|
||||
|
||||
.guideIntro,
|
||||
.guideHint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
@@ -488,10 +494,10 @@
|
||||
.guideSafety {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-business-primary)) 12%, transparent);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
@@ -503,19 +509,10 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.guideNote {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: var(--dsw-alias-bg-layer-3);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.guideList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
@@ -526,38 +523,43 @@
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px 0;
|
||||
border-top: 0.5px solid var(--dsw-alias-border-l4);
|
||||
border-radius: 10px;
|
||||
background: var(--dsw-alias-bg-layer-2);
|
||||
}
|
||||
|
||||
.guideIndex {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
corner-shape: round;
|
||||
background: var(--dsw-alias-bg-layer-3);
|
||||
background: var(--dsw-alias-bg-module-platform);
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
font-weight: 500;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.guideMain {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.guideTitle {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.guideExample {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -581,7 +583,9 @@
|
||||
/* The footer's only action spans the dialog, as the primary action of a short form does. */
|
||||
.wide {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
/* The installing, installed, and failed screens: a head row, the centred
|
||||
@@ -593,7 +597,7 @@
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: 16px 20px 20px;
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
/* Everything under the head: it scrolls when the runs' output makes the screen taller than the card. */
|
||||
@@ -664,6 +668,14 @@
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.wizardIcon[data-state='done'] {
|
||||
color: var(--dsw-alias-state-success-secondary);
|
||||
}
|
||||
|
||||
.wizardIcon[data-state='error'] {
|
||||
color: var(--dsw-alias-state-warn-label);
|
||||
}
|
||||
|
||||
.wizardTitle {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
@@ -714,6 +726,11 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footAction {
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.detailsToggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
import { useEffect, useId, useState, type ReactNode } from 'react'
|
||||
import type { PluginInstallFailureKind } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import {
|
||||
Button, IconChevronDownOutlineRegular, IconChevronLeftOutlineRegular,
|
||||
IconChevronRightOutlineRegular, IconCloseOutlineRegular,
|
||||
Button, IconCheckCircleFillRegular, IconChevronDownOutlineRegular, IconChevronLeftOutlineMedium,
|
||||
IconChevronRightOutlineRegular, IconCloseOutlineMedium,
|
||||
IconPlusOutlineRegular, IconRefreshOutlineRegular, IconTrashOutlineRegular,
|
||||
IconWarningOutlineRegular, Input, Modal,
|
||||
PluginArtworkDefault, PluginArtworkLoop, PluginArtworkSearch, PluginArtworkSubagent, PluginArtworkTeam, PluginArtworkTerminal,
|
||||
@@ -672,19 +672,19 @@ function InstallDialog({
|
||||
)}
|
||||
>
|
||||
<div className={css.installBody}>
|
||||
<label className={css.installField}>
|
||||
<span>{t('installSpecLabel')}</span>
|
||||
<div className={css.installField}>
|
||||
<input
|
||||
type="text"
|
||||
value={install.spec}
|
||||
placeholder={t('installSpecPlaceholder')}
|
||||
disabled={checking}
|
||||
aria-label={t('installSpecLabel')}
|
||||
aria-invalid={install.inputError !== null}
|
||||
aria-describedby={install.inputError === null ? undefined : errorId}
|
||||
onChange={(event) => { onEditSpec(event.currentTarget.value) }}
|
||||
onKeyDown={(event) => { if (event.key === 'Enter' && !empty && !checking) onRun() }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{install.inputError === null
|
||||
? null
|
||||
: <p id={errorId} className={css.inputError} role="alert">{t(INPUT_PROBLEM_KEYS[install.inputError.problem], { reason: install.inputError.reason })}</p>}
|
||||
@@ -701,19 +701,17 @@ function InstallDialog({
|
||||
{guideOpen
|
||||
? (
|
||||
<div id={guideId} className={css.guide} data-install-guide>
|
||||
<p className={css.guideIntro}>{t('installGuideIntro')}</p>
|
||||
<p className={css.guideNote}>{t('installGuideIdNote')}</p>
|
||||
<ol className={css.guideList}>
|
||||
{GUIDE_EXAMPLES.map(({ key, titleKey, exampleKey, hintKey }, index) => (
|
||||
<li key={key} className={css.guideItem}>
|
||||
<span className={css.guideIndex} aria-hidden="true">{index + 1}</span>
|
||||
<div className={css.guideMain}>
|
||||
<span className={css.guideTitle}>{t(titleKey)}</span>
|
||||
<span className={css.guideHint}>{t(hintKey)}</span>
|
||||
<span className={css.guideExample}>
|
||||
<span className={css.guideExampleLabel}>{t('installGuideExampleLabel')}</span>
|
||||
<code>{t(exampleKey)}</code>
|
||||
</span>
|
||||
<span className={css.guideHint}>{t(hintKey)}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -754,7 +752,7 @@ function InstallDialog({
|
||||
? <span />
|
||||
: (
|
||||
<button type="button" className={css.wizardBack} aria-label={t('installEditAria')} disabled={!stoppable} onClick={onCancel}>
|
||||
<IconChevronLeftOutlineRegular aria-hidden="true" />
|
||||
<IconChevronLeftOutlineMedium aria-hidden="true" />
|
||||
<span>{t('installEdit')}</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -765,13 +763,17 @@ function InstallDialog({
|
||||
disabled={pending && phase !== 'running'}
|
||||
onClick={phase === 'running' ? onCancelAndClose : onClose}
|
||||
>
|
||||
<IconCloseOutlineRegular size={14} />
|
||||
<IconCloseOutlineMedium size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className={css.wizardScroll}>
|
||||
<div className={css.wizardHero}>
|
||||
<span className={css.wizardIcon} aria-hidden="true">
|
||||
<StateDot state={pending ? 'ongoing' : phase === 'done' ? 'done' : 'error'} />
|
||||
<span className={css.wizardIcon} data-state={pending ? 'ongoing' : phase === 'done' ? 'done' : 'error'} aria-hidden="true">
|
||||
{pending
|
||||
? <StateDot state="ongoing" size={28} />
|
||||
: phase === 'done'
|
||||
? <IconCheckCircleFillRegular size={28} />
|
||||
: <IconWarningOutlineRegular size={28} />}
|
||||
</span>
|
||||
<h2 className={css.wizardTitle} role={phase === 'failed' ? 'alert' : 'status'}>{heading}</h2>
|
||||
{phase === 'failed' ? <p className={css.wizardSub}>{failureText(install.failure, t)}</p> : null}
|
||||
@@ -808,12 +810,12 @@ function InstallDialog({
|
||||
</button>
|
||||
{pending
|
||||
? (
|
||||
<Button variant="outline" size="sm" disabled={phase !== 'running'} onClick={onCancel}>
|
||||
<Button variant="outline" size="sm" className={css.footAction} disabled={phase !== 'running'} onClick={onCancel}>
|
||||
{t(phase === 'cancelling' ? 'installCancelling' : 'installCancel')}
|
||||
</Button>
|
||||
)
|
||||
: null}
|
||||
{phase === 'failed' && !approvable ? <Button variant="primary" size="sm" onClick={onRun}>{t('installRetry')}</Button> : null}
|
||||
{phase === 'failed' && !approvable ? <Button variant="primary" size="sm" className={css.footAction} onClick={onRun}>{t('installRetry')}</Button> : null}
|
||||
</div>
|
||||
{install.detailsOpen
|
||||
? (
|
||||
|
||||
@@ -55,13 +55,11 @@ export const zh = {
|
||||
installDescription: '输入插件的包名、GitHub 仓库地址或本地目录路径。',
|
||||
installSpecLabel: '包名或地址',
|
||||
installSpecPlaceholder: '例如 @deepseek-ai/dsh-experimental-auto-review',
|
||||
installGuideToggle: '不知道该填什么?',
|
||||
installGuideToggle: '插件安装引导和示例',
|
||||
installGuideHide: '收起引导',
|
||||
installGuideIntro: '从插件的 README 或发布页面复制以下任意一项。',
|
||||
installGuideIdNote: '包名即 npm 包名,常见形式为 dsh-xxx 或 @作者/插件名;插件的显示名称不是包名。',
|
||||
installGuideIdTitle: '包名',
|
||||
installGuideIdExample: '@deepseek-ai/dsh-experimental-auto-review',
|
||||
installGuideIdHint: 'README 安装命令中 dsh plugin add 或 pnpm add 之后的部分。',
|
||||
installGuideIdHint: '插件包名即 npm 包名(如 dsh-xxx 或 @作者/插件名),社区插件的 README 安装命令中 dsh plugin add 或 pnpm add 之后的部分。',
|
||||
installGuideGitTitle: 'GitHub 仓库地址',
|
||||
installGuideGitExample: 'https://github.com/author/dsh-plugin',
|
||||
installGuideGitHint: '插件在 GitHub 上的开源仓库地址,也支持其他 Git 仓库。',
|
||||
@@ -217,13 +215,11 @@ export const en = {
|
||||
installDescription: 'Enter the plugin\'s package name, GitHub repository address, or local directory path.',
|
||||
installSpecLabel: 'Package name or address',
|
||||
installSpecPlaceholder: 'for example @deepseek-ai/dsh-experimental-auto-review',
|
||||
installGuideToggle: 'Not sure what to enter?',
|
||||
installGuideToggle: 'Install guide and examples',
|
||||
installGuideHide: 'Hide the guide',
|
||||
installGuideIntro: 'Copy any one of these from the plugin\'s README or release page.',
|
||||
installGuideIdNote: 'The package name is the npm package name, usually dsh-xxx or @author/plugin; the display name is not one.',
|
||||
installGuideIdTitle: 'Package name',
|
||||
installGuideIdExample: '@deepseek-ai/dsh-experimental-auto-review',
|
||||
installGuideIdHint: 'The part after dsh plugin add or pnpm add in the README\'s install command.',
|
||||
installGuideIdHint: 'The plugin package name is the npm package name (like dsh-xxx or @author/plugin): the part after dsh plugin add or pnpm add in a community plugin\'s README install command.',
|
||||
installGuideGitTitle: 'GitHub repository address',
|
||||
installGuideGitExample: 'https://github.com/author/dsh-plugin',
|
||||
installGuideGitHint: 'The address of the plugin\'s open-source repository on GitHub; other Git hosts work too.',
|
||||
|
||||
@@ -309,17 +309,17 @@ describe('PluginManagerPage', () => {
|
||||
|
||||
it('opens a guide under the field and drops an example into it', () => {
|
||||
const { actions } = renderTab({ install: { ...IDLE_INSTALL, open: true } })
|
||||
expect(screen.queryByText(en.installGuideIntro)).toBeNull()
|
||||
expect(screen.queryByText(en.installGuideIdHint)).toBeNull()
|
||||
const toggle = screen.getByRole('button', { name: en.installGuideToggle })
|
||||
expect(toggle.getAttribute('aria-expanded')).toBe('false')
|
||||
fireEvent.click(toggle)
|
||||
expect(screen.getByRole('button', { name: en.installGuideHide }).getAttribute('aria-expanded')).toBe('true')
|
||||
expect(screen.getByText(en.installGuideIdNote)).toBeTruthy()
|
||||
expect(screen.getByText(en.installGuideIdHint)).toBeTruthy()
|
||||
expect(screen.getByText(en.installGuideGitExample)).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: en.installGuideFillAria.replace('{example}', en.installGuideIdExample) }))
|
||||
expect(actions.editInstallSpec).toHaveBeenCalledExactlyOnceWith(en.installGuideIdExample)
|
||||
fireEvent.click(screen.getByRole('button', { name: en.installGuideHide }))
|
||||
expect(screen.queryByText(en.installGuideIntro)).toBeNull()
|
||||
expect(screen.queryByText(en.installGuideIdHint)).toBeNull()
|
||||
})
|
||||
|
||||
it('opens a bundle\'s page with its facts and rows, and uninstalls from it', () => {
|
||||
|
||||
@@ -586,6 +586,23 @@ export const IconWarningOutlineMedium = (props: IconProps) => (
|
||||
<IconWarningOutlineArtwork {...props} strokeWidth={ICON_MEDIUM_STROKE} />
|
||||
)
|
||||
|
||||
const IconCheckCircleFillArtwork = ({ size = 16, className, strokeWidth }: WeightedIconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" strokeWidth={strokeWidth}>
|
||||
<path d="M28.1936 14.6936L19.8066 23.0806C19.2373 23.65 18.7159 24.1742 18.24 24.5571C17.7389 24.9602 17.1365 25.3359 16.3657 25.458C15.9581 25.5225 15.5428 25.5225 15.1353 25.458C14.3645 25.3359 13.7621 24.9602 13.261 24.5571C12.7851 24.1742 12.2637 23.65 11.6943 23.0806L7.80737 19.1936L10.1936 16.8074L14.0806 20.6943C14.7033 21.317 15.0763 21.6873 15.377 21.9292C15.6523 22.1507 15.7109 22.1325 15.6626 22.1248C15.7208 22.1339 15.7802 22.1339 15.8384 22.1248C15.7901 22.1325 15.8486 22.1507 16.124 21.9292C16.4247 21.6873 16.7977 21.317 17.4204 20.6943L25.8074 12.3074L28.1936 14.6936Z" fill="currentColor" />
|
||||
<path d="M32.8496 18.0005C32.8496 9.79906 26.2019 3.15137 18.0005 3.15137C9.79906 3.15137 3.15137 9.79906 3.15137 18.0005C3.15137 26.2019 9.79906 32.8496 18.0005 32.8496C26.2019 32.8496 32.8496 26.2019 32.8496 18.0005ZM35.7764 18.0005C35.7764 27.8173 27.8173 35.7764 18.0005 35.7764C8.18363 35.7764 0.224609 27.8173 0.224609 18.0005C0.224609 8.18363 8.18363 0.224609 18.0005 0.224609C27.8173 0.224609 35.7764 8.18363 35.7764 18.0005Z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** Regular IconCheckCircleFill artwork (circled check); its fill-only geometry is weight-independent. */
|
||||
export const IconCheckCircleFillRegular = (props: IconProps) => (
|
||||
<IconCheckCircleFillArtwork {...props} strokeWidth={ICON_REGULAR_STROKE} />
|
||||
)
|
||||
|
||||
/** Medium IconCheckCircleFill artwork; it matches Regular because the geometry is fill-only. */
|
||||
export const IconCheckCircleFillMedium = (props: IconProps) => (
|
||||
<IconCheckCircleFillArtwork {...props} strokeWidth={ICON_MEDIUM_STROKE} />
|
||||
)
|
||||
|
||||
const IconUserOutlineArtwork = ({ size = 16, className, strokeWidth }: WeightedIconProps) => (
|
||||
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" strokeWidth={strokeWidth}>
|
||||
<path d="M8 8.5C9.65685 8.5 11 7.15685 11 5.5C11 3.84315 9.65685 2.5 8 2.5C6.34315 2.5 5 3.84315 5 5.5C5 7.15685 6.34315 8.5 8 8.5Z" stroke="currentColor" />
|
||||
|
||||
Reference in new issue
Block a user