Merge remote-tracking branch 'origin/master' into dshw/pr-2423

# Conflicts:
#	packages/client/ui-sidebar/src/client/SidebarRoot.tsx
This commit is contained in:
_Kerman
2026-08-13 04:32:57 +08:00
3517 changed files with 51843 additions and 30957 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@deepseek-ai/dsh-client-ui-sidebar",
"description": "Sidebar plugin: session multi-level tree, search, grouping, state dots",
"version": "0.0.1-rc.2",
"version": "0.0.1-rc.3",
"publishConfig": {
"access": "restricted"
},

View File

@@ -241,11 +241,34 @@
padding-left: 0;
}
/* Foot seat: a pure layout socket pinned under the region; the ui-settings
trigger row inside owns its own geometry (38px wide row / 36px rail
circle) and hover chrome. */
/* Footer seats: additive actions stack above Settings. Each occupant owns its
button geometry and hover chrome. */
.footArea {
flex: none;
display: flex;
flex-direction: column;
}
.settingsArea,
.footerActions {
flex: none;
min-width: 0;
width: 100%;
}
.footerActions {
display: flex;
}
.collapsed .footArea {
align-items: center;
}
.collapsed .settingsArea,
.collapsed .footerActions {
display: flex;
justify-content: center;
width: auto;
}
@media (prefers-reduced-motion: reduce) {

View File

@@ -7,8 +7,8 @@
* same top-down order) on one fade that ends with the slide. The bottom-pinned
* settings control only fades. The workspace/session browsing region between
* the New Session button and the foot is the `sidebar.workspaces` registrant's,
* and the foot is the `sidebar.settings` registrant's; the shell hands them the
* wide flag (plus an expand request callback for the browser).
* and the foot holds `sidebar.settings` plus `sidebar.footer.action`; the shell
* hands them the wide flag (plus an expand request callback for the browser).
*
* The column also owns whether the scroll regions nested in it draw a
* scrollbar at all: the shell tracks the pointer and rebinds ui-theme's
@@ -178,9 +178,14 @@ export function SidebarRoot({
})}
</div>
{/* Foot seat: ui-settings registers the trigger row + panel here. */}
{/* Footer actions stack above Settings in both sidebar widths. */}
<div className={css.footArea}>
{renderSlot('sidebar.settings', { wide })}
<div className={css.footerActions}>
{renderSlot('sidebar.footer.action', { wide })}
</div>
<div className={css.settingsArea}>
{renderSlot('sidebar.settings', { wide })}
</div>
</div>
</div>
)

View File

@@ -4,7 +4,8 @@
* owns column geometry (fold state machine, brand row, New Session);
* everything between the section header and the list bottom is the
* `sidebar.workspaces` registrant's (ui-workspace), and the foot is the
* `sidebar.settings` registrant's (ui-settings).
* `sidebar.settings` registrant's (ui-settings), followed by optional footer
* actions in `sidebar.footer.action`.
*/
import type { PropsLocale, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
// Type-only: pulls ui-layout's SlotMap merge (the 'sidebar' entry) into every
@@ -27,6 +28,11 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* The sidebar passes only its column state — it holds no settings state.
*/
'sidebar.settings': { kind: 'single'; scope: 'root'; owner: SidebarSettingsOwnerProps }
/**
* Optional actions beside Settings at the sidebar foot. Declared by this
* package's 'sidebar' entry; each action receives only the column state.
*/
'sidebar.footer.action': { kind: 'list'; scope: 'root'; owner: SidebarFooterActionOwnerProps }
}
}
@@ -50,6 +56,12 @@ export interface SidebarSettingsOwnerProps {
wide: boolean
}
/** Owner share of an action rendered beside Settings at the sidebar foot. */
export interface SidebarFooterActionOwnerProps {
/** Whether the sidebar renders wide content (false = 56px rail). */
wide: boolean
}
/**
* Registrant-private injected share (arrives via the register inject
* factory). The shell keeps only its own controls: starting a Session from
@@ -72,5 +84,6 @@ export type SidebarRootInjected = {
* seat. No store is registered.
*/
export type SidebarRootComponentProps =
PropsRuntime<'sidebar'> & PropsRenderSlots<'sidebar.workspaces' | 'sidebar.settings'>
PropsRuntime<'sidebar'>
& PropsRenderSlots<'sidebar.workspaces' | 'sidebar.settings' | 'sidebar.footer.action'>
& SidebarRootInjected & PropsLocale<'sidebar'>

View File

@@ -6,7 +6,10 @@ import type { SidebarRootInjected } from './contract/slots.ts'
import { SidebarRoot } from './SidebarRoot.tsx'
import { en, zh, type SidebarKey } from './locales.ts'
export type { SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps } from './contract/slots.ts'
export type {
SidebarFooterActionOwnerProps, SidebarRootComponentProps, SidebarRootInjected,
SidebarSectionOwnerProps, SidebarSettingsOwnerProps,
} from './contract/slots.ts'
export type { SidebarKey } from './locales.ts'
declare module '@deepseek-ai/dsh-client-ui-slots' {
@@ -44,6 +47,7 @@ export function apply(ctx: ClientContext): void {
children: {
'sidebar.workspaces': { kind: 'single', scope: 'root' },
'sidebar.settings': { kind: 'single', scope: 'root' },
'sidebar.footer.action': { kind: 'list', scope: 'root' },
},
inject: injectProps,
}, SidebarRoot),

View File

@@ -3,6 +3,7 @@
exports[`sidebar shell snapshots > renders the collapsed rail after the crossfade settles, in place 1`] = `
<div
data-slot="sidebar"
style="display: contents;"
>
<div
class="root collapsed railIn quietBars"
@@ -52,10 +53,32 @@ exports[`sidebar shell snapshots > renders the collapsed rail after the crossfad
</button>
<div
class="regionArea"
/>
>
<div
data-slot="sidebar.workspaces"
style="display: contents;"
/>
</div>
<div
class="footArea"
/>
>
<div
class="footerActions"
>
<div
data-slot="sidebar.footer.action"
style="display: contents;"
/>
</div>
<div
class="settingsArea"
>
<div
data-slot="sidebar.settings"
style="display: contents;"
/>
</div>
</div>
</div>
</div>
`;
@@ -63,6 +86,7 @@ exports[`sidebar shell snapshots > renders the collapsed rail after the crossfad
exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsule, empty holes) 1`] = `
<div
data-slot="sidebar"
style="display: contents;"
>
<div
class="root quietBars"
@@ -122,10 +146,32 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul
</button>
<div
class="regionArea"
/>
>
<div
data-slot="sidebar.workspaces"
style="display: contents;"
/>
</div>
<div
class="footArea"
/>
>
<div
class="footerActions"
>
<div
data-slot="sidebar.footer.action"
style="display: contents;"
/>
</div>
<div
class="settingsArea"
>
<div
data-slot="sidebar.settings"
style="display: contents;"
/>
</div>
</div>
</div>
</div>
`;
@@ -133,6 +179,7 @@ exports[`sidebar shell snapshots > renders the expanded column (wordmark, capsul
exports[`sidebar shell snapshots > renders the expanded column in the default locale (zh, no setLocale) 1`] = `
<div
data-slot="sidebar"
style="display: contents;"
>
<div
class="root quietBars"
@@ -192,10 +239,32 @@ exports[`sidebar shell snapshots > renders the expanded column in the default lo
</button>
<div
class="regionArea"
/>
>
<div
data-slot="sidebar.workspaces"
style="display: contents;"
/>
</div>
<div
class="footArea"
/>
>
<div
class="footerActions"
>
<div
data-slot="sidebar.footer.action"
style="display: contents;"
/>
</div>
<div
class="settingsArea"
>
<div
data-slot="sidebar.settings"
style="display: contents;"
/>
</div>
</div>
</div>
</div>
`;

View File

@@ -1,22 +1,22 @@
/** Sidebar shell slot registration and its plain runtime/layout callbacks. */
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client'
import type { SidebarRootInjected } from '@deepseek-ai/dsh-client-ui-sidebar/client'
async function bench(declare = true) {
const ctx = new Context()
await ctx.plugin(SlotsService).await()
await ctx.plugin(SlotRegistry).await()
const layout = { toggleSidebar: vi.fn() }
const workspaces = { startSession: vi.fn() }
const sessions = { open: vi.fn(), clear: vi.fn() }
ctx.provide('layout', layout)
ctx.provide('sessions', sessions as never)
ctx.provide('workspaces', workspaces as never)
ctx.provide('locale', new LocaleService(ctx))
const slots = ctx.get('slots') as SlotsService
ctx.provide('locale', new LocaleRuntime(ctx))
const slots = ctx.get('slots') as SlotRegistry
if (declare) {
slots.register(
{ name: 'root', children: { 'sidebar': { kind: 'single', scope: 'root' } } } as never,
@@ -31,11 +31,13 @@ describe('ui-sidebar apply', () => {
expect(inject).toEqual(['slots', 'layout', 'sessions', 'workspaces', 'locale'])
})
it('registers the shell and declares the browsing-region hole', async () => {
it('registers the shell and declares its child seats', async () => {
const b = await bench()
await b.ctx.plugin({ inject: [...inject], apply }).await()
expect(b.slots.entries('sidebar')).toHaveLength(1)
expect(b.slots.spec('sidebar.workspaces')).toEqual({ kind: 'single', scope: 'root' })
expect(b.slots.spec('sidebar.settings')).toEqual({ kind: 'single', scope: 'root' })
expect(b.slots.spec('sidebar.footer.action')).toEqual({ kind: 'list', scope: 'root' })
// Copy rides the standard locale seat, not the inject face.
expect(b.slots.entries('sidebar')[0]!.locale).toBe('sidebar')
const injected = (b.slots.entries('sidebar')[0]!.inject as () => SidebarRootInjected)()
@@ -61,5 +63,6 @@ describe('ui-sidebar apply', () => {
await fiber.dispose()
expect(b.slots.entries('sidebar')).toHaveLength(0)
expect(b.slots.spec('sidebar.workspaces')).toBeUndefined()
expect(b.slots.spec('sidebar.footer.action')).toBeUndefined()
})
})

View File

@@ -1,12 +1,12 @@
import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import * as SidebarInvariant from '@deepseek-ai/dsh-client-ui-sidebar/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
describe('invariant companion', () => {
it('registers under the package name with an empty installer', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(InvariantRegistry, { enabled: true })
await expect(ctx.plugin(SidebarInvariant).await()).resolves.toBeDefined()
})

View File

@@ -1,7 +1,10 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import type { SidebarRootComponentProps, SidebarSectionOwnerProps, SidebarSettingsOwnerProps } from '../src/client/contract/slots.ts'
import type {
SidebarFooterActionOwnerProps, SidebarRootComponentProps, SidebarSectionOwnerProps,
SidebarSettingsOwnerProps,
} from '../src/client/contract/slots.ts'
import { SidebarRoot } from '../src/client/SidebarRoot.tsx'
import { en } from '../src/client/locales.ts'
@@ -23,17 +26,25 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w
const toggleSidebar = vi.fn()
let regionOwner: SidebarSectionOwnerProps | undefined
let settingsOwner: SidebarSettingsOwnerProps | undefined
let footerActionOwner: SidebarFooterActionOwnerProps | undefined
let current = { collapsed, width }
const root = () => (
<SidebarRoot
collapsed={current.collapsed} width={current.width}
useSessions={neverHook} useWorkspaces={neverHook}
startSession={startSession} toggleSidebar={toggleSidebar} t={t}
renderSlot={((key: string, owner: SidebarSectionOwnerProps | SidebarSettingsOwnerProps) => {
renderSlot={((
key: string,
owner: SidebarFooterActionOwnerProps | SidebarSectionOwnerProps | SidebarSettingsOwnerProps,
) => {
if (key === 'sidebar.settings') {
settingsOwner = owner
return <div data-testid="settings-seat" data-wide={owner.wide} />
}
if (key === 'sidebar.footer.action') {
footerActionOwner = owner
return <div data-testid="footer-action-seat" data-wide={owner.wide} />
}
regionOwner = owner as SidebarSectionOwnerProps
return <div data-testid="region" data-wide={owner.wide} />
}) as SidebarRootComponentProps['renderSlot']}
@@ -51,6 +62,10 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w
if (settingsOwner === undefined) throw new Error('settings owner not rendered')
return settingsOwner
},
footerActionOwner: () => {
if (footerActionOwner === undefined) throw new Error('footer action owner not rendered')
return footerActionOwner
},
rerender(next: Partial<typeof current>) {
current = { ...current, ...next }
view.rerender(root())
@@ -75,6 +90,7 @@ describe('SidebarRoot shell', () => {
expect(b.regionOwner().wide).toBe(true)
// The settings seat rides the same wide flag (ui-settings renders the row).
expect(b.settingsOwner().wide).toBe(true)
expect(b.footerActionOwner().wide).toBe(true)
// Expanded: the request is a no-op (no accidental collapse).
b.regionOwner().expandSidebar()
expect(b.toggleSidebar).not.toHaveBeenCalled()
@@ -89,6 +105,7 @@ describe('SidebarRoot shell', () => {
vi.advanceTimersByTime(200)
b.rerender({})
expect(b.regionOwner().wide).toBe(false)
expect(b.footerActionOwner().wide).toBe(false)
expect(screen.getByTestId('region')).toBeTruthy()
b.regionOwner().expandSidebar()
expect(b.toggleSidebar).toHaveBeenCalledOnce()

View File

@@ -11,7 +11,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { act, cleanup, waitFor } from '@testing-library/react'
import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client'
// The service reads its initial locale from the browser; these specs assert
@@ -29,7 +29,7 @@ afterEach(cleanup)
async function bench(options: { locale?: 'en' } = {}) {
const runtime = await SlotTestRuntime.create()
runtime.provide('layout', { toggleSidebar: vi.fn() })
const locale = new LocaleService(runtime.ctx)
const locale = new LocaleRuntime(runtime.ctx)
if (options.locale === 'en') locale.setLocale('en')
runtime.provide('locale', locale)
runtime.slots.installLocale(locale)

View File

@@ -30,7 +30,7 @@
"path": "../locale"
},
{
"path": "../../support/invariants"
"path": "../../runtime-diagnostics/invariants"
}
]
}