feat(ui-settings): add an About section with company copy and version check

Registers a new `settings.section` (id `about`) in ui-settings-general that
introduces PineSound, names the product build, shows the current version, and
offers a check-updates control (a client-side placeholder comparing against a
known-latest constant pending a real update channel). Adds a 16px info icon to
ui-primitives and wires the section into the settings nav. Tests cover the
registration and the About component; READMEs and translation-pairing hashes
are updated.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-14 17:39:02 +08:00
parent a8ec40ad34
commit 1031b63100
13 changed files with 231 additions and 15 deletions
@@ -6,6 +6,7 @@ import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
import { AboutSection } from '../src/client/AboutSection.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx'
@@ -73,6 +74,10 @@ function generalEntry(slots: SlotRegistry) {
return slots.entries('settings.section').find(e => e.component === GeneralSection)
}
function aboutEntry(slots: SlotRegistry) {
return slots.entries('settings.section').find(e => e.component === AboutSection)
}
describe('ui-settings-general apply', () => {
it('declares the services it uses', () => {
expect(inject).toEqual(['slots', 'locale', 'connection'])
@@ -91,6 +96,9 @@ describe('ui-settings-general apply', () => {
expect(resolveSlotLabel(entry.options.label)).toBe('通用设置')
expect(before.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
expect(before.slots.entries('settings.general.item')).toEqual([])
const about = aboutEntry(before.slots)!
expect(about.options).toMatchObject({ id: 'about', order: 100 })
expect(resolveSlotLabel(about.options.label)).toBe('关于')
// The onboarding hole stays declared for feature-owned steps; this plugin
// no longer seats one.
expect(before.slots.entries('settings.onboarding')).toEqual([])
@@ -110,7 +118,7 @@ describe('ui-settings-general apply', () => {
for (const [name, component] of SEATS) {
expect(after.slots.entries(name)[0]!.component).toBe(component)
// The self-inflicted ledger notifications hit the duplicate guard.
expect(after.slots.entries(name)).toHaveLength(1)
expect(after.slots.entries(name)).toHaveLength(name === 'settings.section' ? 2 : 1)
}
await vi.waitFor(() => {
expect(after.slots.spec('settings.general.item')).toEqual({ kind: 'list', scope: 'root' })
@@ -142,7 +150,7 @@ describe('ui-settings-general apply', () => {
// subscription), not re-registration.
SEATS.forEach(([name], i) => {
expect(b.slots.getVersion(name)).toBe(zhVersions[i]!)
expect(b.slots.entries(name)).toHaveLength(1)
expect(b.slots.entries(name)).toHaveLength(name === 'settings.section' ? 2 : 1)
})
expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('General')
b.locale.setLocale('zh')
@@ -4,6 +4,8 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type { GeneralSectionComponentProps } from '../src/client/GeneralSection.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
import { AboutSection } from '../src/client/AboutSection.tsx'
import type { AboutSectionComponentProps } from '../src/client/AboutSection.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import type { TriggerContentProps } from '../src/client/chrome.tsx'
import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx'
@@ -58,6 +60,28 @@ describe('GeneralSection', () => {
})
})
describe('AboutSection', () => {
const aboutProps: AboutSectionComponentProps = { close: vi.fn(), t } as never
it('renders the company copy and the current version', () => {
render(<AboutSection {...aboutProps} />)
expect(screen.getByText('PineSound')).toBeTruthy()
expect(screen.getByText('AI audio creation platform')).toBeTruthy()
expect(screen.getByText(en['about.currentVersion'])).toBeTruthy()
expect(screen.getByText('0.1.0-rc.5')).toBeTruthy()
expect(screen.getByRole('button', { name: en['about.checkUpdates'] })).toBeTruthy()
})
it('checks for updates and reports the latest version', async () => {
render(<AboutSection {...aboutProps} />)
fireEvent.click(screen.getByRole('button', { name: en['about.checkUpdates'] }))
expect(screen.getByText(en['about.checking'])).toBeTruthy()
await waitFor(() => {
expect(screen.getByRole('status').textContent).toContain(en['about.upToDate'])
})
})
})
describe('SettingsDocumentAction', () => {
it('appears only for a file-backed provider and requests its Host-owned document', async () => {
const openDocument = vi.fn(() => Promise.resolve({
@@ -76,10 +76,11 @@ describe('ui-settings apply', () => {
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const { sections } = injectedOf(b.slots).hooks
// This package registers the General section itself; every other section
// arrives from a feature registrant.
// This package registers the General and About sections itself; every
// other section arrives from a feature registrant.
const GENERAL = { id: 'general', order: 0, label: 'general.nav' }
expect(sections.getSnapshot()).toEqual([GENERAL])
const ABOUT = { id: 'about', order: 100, label: 'about.nav' }
expect(sections.getSnapshot()).toEqual([GENERAL, ABOUT])
b.slots.register({ name: 'settings.section', id: 'z', order: 20, label: 'Z' } as never, () => null)
// No order and no label: both projection defaults apply.
b.slots.register({ name: 'settings.section', id: 'a' } as never, () => null)
@@ -88,6 +89,7 @@ describe('ui-settings apply', () => {
GENERAL,
{ id: 'a', order: 0, label: '' },
{ id: 'z', order: 20, label: 'Z' },
ABOUT,
])
// Snapshot identity is stable until the ledger moves (uSES contract).
expect(sections.getSnapshot()).toBe(rows)