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:
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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 路径)。
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user