fix: harden skill market theme and loading states

This commit is contained in:
程序员阿江(Relakkes) 2026-07-04 22:21:22 +08:00
parent 3fc6b7f7df
commit 977cd6e87a
8 changed files with 58 additions and 3 deletions

View File

@ -2086,6 +2086,7 @@ export const en = {
'skillCenter.marketplace.drawer.metadata': 'Metadata',
'skillCenter.marketplace.drawer.links': 'Links',
'skillCenter.marketplace.loadingCard': 'Loading marketplace skills',
'skillCenter.marketplace.loadingDetail': 'Loading skill detail',
'skillCenter.marketplace.previewTruncated': 'Truncated',
'skillCenter.marketplace.previewUnavailable': 'Preview unavailable',
'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub does not expose a safe raw file preview endpoint yet.',

View File

@ -2088,6 +2088,7 @@ export const jp: Record<TranslationKey, string> = {
'skillCenter.marketplace.drawer.metadata': 'メタデータ',
'skillCenter.marketplace.drawer.links': 'リンク',
'skillCenter.marketplace.loadingCard': 'マーケットスキルを読み込み中',
'skillCenter.marketplace.loadingDetail': 'スキル詳細を読み込み中',
'skillCenter.marketplace.previewTruncated': '切り詰め済み',
'skillCenter.marketplace.previewUnavailable': 'プレビュー不可',
'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub はまだ安全な raw ファイルプレビュー API を提供していません。',

View File

@ -2088,6 +2088,7 @@ export const kr: Record<TranslationKey, string> = {
'skillCenter.marketplace.drawer.metadata': '메타데이터',
'skillCenter.marketplace.drawer.links': '링크',
'skillCenter.marketplace.loadingCard': '마켓 스킬 로드 중',
'skillCenter.marketplace.loadingDetail': '스킬 상세 로드 중',
'skillCenter.marketplace.previewTruncated': '잘림',
'skillCenter.marketplace.previewUnavailable': '미리보기 불가',
'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub는 아직 안전한 원본 파일 미리보기 API를 제공하지 않습니다.',

View File

@ -2088,6 +2088,7 @@ export const zh: Record<TranslationKey, string> = {
'skillCenter.marketplace.drawer.metadata': '中繼資料',
'skillCenter.marketplace.drawer.links': '連結',
'skillCenter.marketplace.loadingCard': '正在載入市場技能',
'skillCenter.marketplace.loadingDetail': '正在載入技能詳情',
'skillCenter.marketplace.previewTruncated': '已截斷',
'skillCenter.marketplace.previewUnavailable': '暫時無法預覽',
'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub 目前尚未提供安全的原始檔案預覽介面。',

View File

@ -2088,6 +2088,7 @@ export const zh: Record<TranslationKey, string> = {
'skillCenter.marketplace.drawer.metadata': '元数据',
'skillCenter.marketplace.drawer.links': '链接',
'skillCenter.marketplace.loadingCard': '正在加载市场技能',
'skillCenter.marketplace.loadingDetail': '正在加载技能详情',
'skillCenter.marketplace.previewTruncated': '已截断',
'skillCenter.marketplace.previewUnavailable': '暂时无法预览',
'skillCenter.marketplace.previewUnavailable.skillhub': 'SkillHub 目前还没有提供安全的原始文件预览接口。',

View File

@ -245,10 +245,36 @@ describe('SkillCenter', () => {
render(<SkillCenter />)
expect(screen.getByTestId('skill-marketplace-loading')).toBeInTheDocument()
const loadingRegion = screen.getByTestId('skill-marketplace-loading')
expect(loadingRegion).toBeInTheDocument()
expect(loadingRegion).toHaveAttribute('role', 'status')
expect(loadingRegion).toHaveAttribute('aria-busy', 'true')
expect(screen.getByRole('status', { name: 'Loading marketplace skills' })).toBe(loadingRegion)
expect(screen.getAllByTestId('skill-card-skeleton')).toHaveLength(8)
})
it('keeps the detail drawer in the body modal layer for uniform theme masking', async () => {
render(<SkillCenter />)
fireEvent.click(await screen.findByRole('button', { name: 'PPT Generator' }))
const detailLayer = await screen.findByTestId('skill-market-detail-layer')
expect(detailLayer.parentElement).toBe(document.body)
expect(detailLayer).toHaveClass('fixed', 'inset-0', 'z-50')
expect(detailLayer.querySelector('.skill-market-detail-scrim')).not.toBeNull()
})
it('announces detail drawer skeleton loading without visible copy', async () => {
mockedSkillMarketApi.detail.mockReturnValue(new Promise(() => {}))
render(<SkillCenter />)
fireEvent.click(await screen.findByRole('button', { name: 'PPT Generator' }))
const detailLayer = screen.getByTestId('skill-market-detail-layer')
const detailLoading = within(detailLayer).getByRole('status', { name: 'Loading skill detail' })
expect(detailLoading).toHaveAttribute('aria-busy', 'true')
expect(within(detailLoading).getByText('Loading skill detail')).toHaveClass('sr-only')
})
it('places install decision, blocking reason, and file preview in the drawer decision area', async () => {
mockedSkillMarketApi.detail.mockResolvedValue({
detail: makeDetail({
@ -267,6 +293,7 @@ describe('SkillCenter', () => {
fireEvent.click(await screen.findByRole('button', { name: 'PPT Generator' }))
const dialog = await screen.findByRole('dialog', { name: 'PPT Generator' })
expect(within(dialog).getByRole('heading', { name: 'Install decision' })).toBeInTheDocument()
expect(within(dialog).getByRole('button', { name: 'Blocked' })).toBeDisabled()
expect(within(dialog).getByText(/full package safety scan/i)).toBeInTheDocument()
expect(within(dialog).getByText('File preview')).toBeInTheDocument()

View File

@ -787,7 +787,13 @@ function SkillMarketDetailPanel({
</button>
</div>
<div className="border-b border-[var(--color-border)] bg-[var(--color-surface)] p-5">
<div
aria-labelledby="skill-market-detail-decision-title"
className="border-b border-[var(--color-border)] bg-[var(--color-surface)] p-5"
>
<h4 id="skill-market-detail-decision-title" className="sr-only">
{t('skillCenter.marketplace.drawer.decision')}
</h4>
<button
type="button"
onClick={() => (canOpenInstalled ? onOpenInstalled(detail) : onInstall())}
@ -1097,6 +1103,8 @@ function MarketplaceSkeletonGrid() {
return (
<div
data-testid="skill-marketplace-loading"
role="status"
aria-busy="true"
aria-label={t('skillCenter.marketplace.loadingCard')}
className="grid gap-2.5 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5"
>
@ -1120,8 +1128,16 @@ function MarketplaceSkeletonGrid() {
}
function DetailDrawerSkeleton() {
const t = useTranslation()
return (
<div className="flex h-full flex-col">
<div
role="status"
aria-busy="true"
aria-label={t('skillCenter.marketplace.loadingDetail')}
className="flex h-full flex-col"
>
<span className="sr-only">{t('skillCenter.marketplace.loadingDetail')}</span>
<div className="border-b border-[var(--color-border)] px-5 py-4">
<div className="h-5 w-28 animate-pulse rounded bg-[var(--color-surface-container-high)]" />
<div className="mt-3 h-6 w-2/3 animate-pulse rounded bg-[var(--color-surface-container-high)]" />

View File

@ -1082,6 +1082,13 @@ button, input, textarea, select, a, [role="button"] {
-webkit-backdrop-filter: blur(1.5px);
}
@media (prefers-reduced-motion: reduce) {
.skill-market-detail-scrim {
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
}
.sidebar-shell {
width: var(--sidebar-width);
flex-shrink: 0;