Merge remote-tracking branch 'origin/master' into dshw/pr-2423
# Conflicts: # packages/client/ui-sidebar/src/client/SidebarRoot.tsx
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"path": "../locale"
|
||||
},
|
||||
{
|
||||
"path": "../../support/invariants"
|
||||
"path": "../../runtime-diagnostics/invariants"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user