Merge remote-tracking branch 'origin/master' into worktree/i18n-complete-non-readme

# Conflicts:
#	packages/client/i18n/README.md
#	packages/client/locale/README.i18n.yaml
#	packages/client/locale/README.zh.md
#	packages/client/ui-layout/README.md
#	packages/client/ui-theme/README.md
This commit is contained in:
Tianyi Cui
2026-07-26 14:37:46 +08:00
136 changed files with 4696 additions and 459 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 3a4c88872866c29777421f68bd9140eda98f8541
README.zh.md: e7b1bba52f858db70a9982164c63b5518ed9980a
README.md: 93a1f15a5802f94a0ebe930dda1dbd4fbc7343c9
README.zh.md: 1ef636dbd00c894c8312ab1fbfa9a96af45956f0

View File

@@ -6,7 +6,9 @@ Sidebar plugin: real Host Workspaces in stable Host order, each containing its `
New Session starts the runtime's page-local frontend Session Intent; a real Workspace's "+" starts one targeted to that Workspace. The Workspace header "+" opens ui-workspace's shared picker, whose selection also targets a frontend Session. A Workspace Intent does not appear in the sidebar.
`SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared `sidebar.workspace` child slot, and injected `startSession`, `open`, and sidebar-toggle callbacks. There is no plugin store: `deriveGroups` consumes object-layer snapshots and component-local expansion/search state.
`SidebarRootComponentProps` composes the layout owner share, the global `useSessions` and `useWorkspaces` hooks, the declared `sidebar.workspace` and `sidebar.settings` child slots, and injected `startSession`, `open`, and sidebar-toggle callbacks. There is no plugin store: `deriveGroups` consumes object-layer snapshots and component-local expansion/search state.
The foot is the `sidebar.settings` seat: the sidebar renders only the bottom-pinned layout slot and shares its column state (`wide`); ui-settings registers the trigger row and settings panel there.
The `/client` export surface is the plugin body (`apply`/`inject`) plus the contract types only — SidebarRoot, the row components, and the tree derivation are internal (the slot registration closes over them; tests import src paths directly).

View File

@@ -6,7 +6,9 @@
New Session 会启动运行时的页面局部前端 Session Intent真实 Workspace 的「+」会启动一项以该 Workspace 为目标的 Intent。Workspace 标题栏的「+」打开 ui-workspace 的共享选择器,选择结果同样以一个前端 Session 为目标。Workspace Intent 不会出现在侧边栏中。
`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions``useWorkspaces` hook、已声明的 `sidebar.workspace` 子 slot以及注入的 `startSession``open` 和侧边栏切换回调。这里没有插件 store`deriveGroups` 消费对象层快照与组件局部的展开/搜索状态。
`SidebarRootComponentProps` 组合布局 owner share、全局 `useSessions``useWorkspaces` hook、已声明的 `sidebar.workspace``sidebar.settings` 子 slot以及注入的 `startSession``open` 和侧边栏切换回调。这里没有插件 store`deriveGroups` 消费对象层快照与组件局部的展开/搜索状态。
页脚承载 `sidebar.settings`:侧边栏只渲染固定在底部的布局 slot并共享其栏状态`wide`ui-settings 在此注册触发行和设置面板。
`/client` 导出表层只包含插件主体(`apply``inject`及契约类型SidebarRoot、行组件和树派生均属于内部实现slot 注册通过闭包引用它们;测试直接导入 src 路径)。

View File

@@ -48,7 +48,7 @@
refresh straight into the collapsed state renders statically. */
.railIn .iconButton,
.railIn .newSession,
.railIn .foot {
.railIn .footArea {
animation: rail-in 150ms var(--ds-ease-in-out) 100ms backwards;
}
@@ -193,45 +193,11 @@
overflow: hidden;
}
/* Foot: settings entry (figma 133:7668, 49 hug): the former 18/10 vertical
margins fold into the row so the hover pill spans the full 49px. */
.foot {
/* Foot seat: a pure layout socket pinned under the region; the ui-settings
trigger row inside owns its own geometry (49px wide row / 36px rail
circle) and hover chrome. */
.footArea {
flex: none;
display: flex;
align-items: center;
gap: 8px;
height: 49px;
margin: 8px 0 0; /* + 49px row + root padBottom 6 keeps the old 57px band */
padding: 0 2px 0 6px;
border-radius: 12px;
cursor: pointer;
overflow: hidden;
color: var(--dsw-alias-label-primary);
}
.foot:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
/* Rail settings: the same 36x36 circle box as the other rail controls. */
.collapsed .foot {
width: 36px;
height: 36px;
margin: 18px 0 10px;
justify-content: center;
gap: 0;
padding: 0;
border-radius: 50%;
}
.footLabel {
max-width: 120px;
overflow: hidden;
white-space: nowrap;
}
.collapsed .footLabel {
max-width: 0;
}
@media (prefers-reduced-motion: reduce) {
@@ -239,7 +205,7 @@
.fading > *,
.railIn .iconButton,
.railIn .newSession,
.railIn .foot {
.railIn .footArea {
transition: none;
animation: none;
}

View File

@@ -5,14 +5,15 @@
* mid-slide. At settle the wide-only content unmounts and the control rows
* snap to the 56px rail (one icon each, same top-down order) fading in as the
* slide ends. The workspace/session browsing region between the New Session
* button and the foot is the `sidebar.workspaces` registrant's; the shell
* hands it the wide flag and an expand request callback.
* 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).
*/
import { useEffect, useRef, useState } from 'react'
import clsx from 'clsx'
import {
BrandWordmark, FishLogo,
IconNewChatOutline16, IconPanelLeftOutline16, IconSettingsOutline14,
IconNewChatOutline16, IconPanelLeftOutline16,
Tooltip,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { SidebarRootComponentProps } from './contract/slots.ts'
@@ -102,9 +103,9 @@ export function SidebarRoot({
})}
</div>
<div className={css.foot} role="button" tabIndex={0} aria-label="Settings">
<IconSettingsOutline14 size={wide ? 14 : 18} />
{wide && <span className={clsx(css.footLabel, css.wide)}>Settings</span>}
{/* Foot seat: ui-settings registers the trigger row + panel here. */}
<div className={css.footArea}>
{renderSlot('sidebar.settings', { wide })}
</div>
</div>
)

View File

@@ -1,9 +1,10 @@
/**
* Sidebar slot contract: the registrant-side props composition for the
* layout-owned `sidebar` slot, plus the workspace-browser hole this shell
* declares. The shell owns column geometry (fold state machine, brand row,
* New Session, Settings); everything between the section header and the list
* bottom is the `sidebar.workspaces` registrant's (ui-workspace).
* layout-owned `sidebar` slot, plus the holes this shell declares. The shell
* 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).
*/
import type { PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
// Type-only: pulls ui-layout's SlotMap merge (the 'sidebar' entry) into every
@@ -20,6 +21,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
* registers the browser.
*/
'sidebar.workspaces': { kind: 'single'; scope: 'root'; owner: SidebarSectionOwnerProps }
/**
* The settings seat at the sidebar foot. Declared by this package's
* 'sidebar' entry; ui-settings registers its trigger row + modal panel.
* The sidebar passes only its column state — it holds no settings state.
*/
'sidebar.settings': { kind: 'single'; scope: 'root'; owner: SidebarSettingsOwnerProps }
}
}
@@ -34,6 +41,15 @@ export interface SidebarSectionOwnerProps {
expandSidebar: () => void
}
/**
* Owner share of the sidebar settings seat: the column display state the
* occupant's trigger row must render against (wide row vs rail icon).
*/
export interface SidebarSettingsOwnerProps {
/** 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
@@ -47,8 +63,8 @@ export type SidebarRootInjected = {
}
/**
* Full component props: layout owner state/actions plus the browser hole's
* render share and this package's injected callbacks. No store is registered.
* Full component props: layout owner state/actions plus the declared holes'
* render shares and this package's injected callbacks. No store is registered.
*/
export type SidebarRootComponentProps =
PropsRuntime<'sidebar'> & PropsRenderSlots<'sidebar.workspaces'> & SidebarRootInjected
PropsRuntime<'sidebar'> & PropsRenderSlots<'sidebar.workspaces' | 'sidebar.settings'> & SidebarRootInjected

View File

@@ -3,7 +3,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
import type { SidebarRootInjected } from './contract/slots.ts'
import { SidebarRoot } from './SidebarRoot.tsx'
export type { SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps } from './contract/slots.ts'
export type { SidebarRootComponentProps, SidebarRootInjected, SidebarSectionOwnerProps, SidebarSettingsOwnerProps } from './contract/slots.ts'
/** Services required by the sidebar plugin. */
export const inject = ['slots', 'layout', 'workspaces']
@@ -20,8 +20,12 @@ export function apply(ctx: ClientContext): void {
() => ctx.slots.register({
name: 'sidebar',
// The shell owns geometry; ui-workspace registers the whole browsing
// region (header, search, session list, workspace dialogs) here.
children: { 'sidebar.workspaces': { kind: 'single', scope: 'root' } },
// region (header, search, session list, workspace dialogs), ui-settings
// registers the foot trigger + settings panel.
children: {
'sidebar.workspaces': { kind: 'single', scope: 'root' },
'sidebar.settings': { kind: 'single', scope: 'root' },
},
inject: injectProps,
}, SidebarRoot),
'ui-sidebar: slot registration',

View File

@@ -1,7 +1,7 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import type { SidebarRootComponentProps, SidebarSectionOwnerProps } from '../src/client/contract/slots.ts'
import type { SidebarRootComponentProps, SidebarSectionOwnerProps, SidebarSettingsOwnerProps } from '../src/client/contract/slots.ts'
import { SidebarRoot } from '../src/client/SidebarRoot.tsx'
afterEach(() => {
@@ -17,14 +17,19 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w
const startSession = vi.fn()
const toggleSidebar = vi.fn()
let regionOwner: SidebarSectionOwnerProps | undefined
let settingsOwner: SidebarSettingsOwnerProps | undefined
let current = { collapsed, width }
const root = () => (
<SidebarRoot
collapsed={current.collapsed} width={current.width}
useSessions={neverHook} useWorkspaces={neverHook}
startSession={startSession} toggleSidebar={toggleSidebar}
renderSlot={((_key: string, owner: SidebarSectionOwnerProps) => {
regionOwner = owner
renderSlot={((key: string, owner: SidebarSectionOwnerProps | SidebarSettingsOwnerProps) => {
if (key === 'sidebar.settings') {
settingsOwner = owner as SidebarSettingsOwnerProps
return <div data-testid="settings-seat" data-wide={owner.wide} />
}
regionOwner = owner as SidebarSectionOwnerProps
return <div data-testid="region" data-wide={owner.wide} />
}) as SidebarRootComponentProps['renderSlot']}
/>
@@ -37,6 +42,10 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w
if (regionOwner === undefined) throw new Error('region owner not rendered')
return regionOwner
},
settingsOwner: () => {
if (settingsOwner === undefined) throw new Error('settings owner not rendered')
return settingsOwner
},
rerender(next: Partial<typeof current>) {
current = { ...current, ...next }
view.rerender(root())
@@ -56,6 +65,8 @@ describe('SidebarRoot shell', () => {
it('hands the region its wide flag and clamps expandSidebar to the collapsed state', () => {
const b = mountShell()
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)
// Expanded: the request is a no-op (no accidental collapse).
b.regionOwner().expandSidebar()
expect(b.toggleSidebar).not.toHaveBeenCalled()