wip
fix: docs
This commit is contained in:
@@ -49,6 +49,7 @@
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/dsh-client-locale": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-test-runtime": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
|
||||
@@ -10,8 +10,13 @@
|
||||
import { Fragment, useEffect, useRef, useSyncExternalStore } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useAnchoredMaxHeight } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import css from './MenuView.module.css'
|
||||
import type { MenuViewInjected } from './slots.ts'
|
||||
import type { MenuKey } from './locales.ts'
|
||||
|
||||
/** Full menu props: injected face + the locale seat. */
|
||||
export type MenuViewProps = MenuViewInjected & PropsLocale<'slash.menu'>
|
||||
|
||||
/** Design cap on the list height (figma SLASH 39:26572 MenuDropdown). */
|
||||
const MAX_HEIGHT = 320
|
||||
@@ -23,10 +28,10 @@ function optionId(source: string, index: number): string {
|
||||
|
||||
/**
|
||||
* Render the candidate menu overlay entry.
|
||||
* @param props - injected face: the menu store, the pick route, and the menu-namespace translator.
|
||||
* @param props - injected face (the menu store and the pick route); `t` rides the standard locale seat.
|
||||
* @returns the dropdown while open; null while closed.
|
||||
*/
|
||||
export function MenuView({ menu, onPick, onDismiss, t }: MenuViewInjected) {
|
||||
export function MenuView({ menu, onPick, onDismiss, t }: MenuViewProps) {
|
||||
const state = useSyncExternalStore(
|
||||
fn => menu.subscribe(fn),
|
||||
() => menu.getSnapshot(),
|
||||
@@ -65,7 +70,7 @@ export function MenuView({ menu, onPick, onDismiss, t }: MenuViewInjected) {
|
||||
className={css.menu}
|
||||
style={{ maxHeight }}
|
||||
role="listbox"
|
||||
aria-label="Trigger suggestions"
|
||||
aria-label={t('suggestions.aria')}
|
||||
aria-activedescendant={highlight !== null ? optionId(highlight.source, highlight.index) : undefined}
|
||||
>
|
||||
<div className={css.viewport}>
|
||||
@@ -73,7 +78,10 @@ export function MenuView({ menu, onPick, onDismiss, t }: MenuViewInjected) {
|
||||
? null
|
||||
: (
|
||||
<Fragment key={group.source}>
|
||||
<div className={css.groupTitle} role="presentation" data-source={group.source}>{t(group.source)}</div>
|
||||
{/* Source names key the dictionary open-endedly: the lookup chain
|
||||
returns an unknown key verbatim, so an unregistered source
|
||||
shows its raw name — hence the cast past the typed key union. */}
|
||||
<div className={css.groupTitle} role="presentation" data-source={group.source}>{t(group.source as MenuKey)}</div>
|
||||
{group.status === 'pending'
|
||||
? <div className={css.loading} data-source={group.source}>{t('loading')}</div>
|
||||
: group.items.map((item, index) => {
|
||||
|
||||
@@ -10,11 +10,14 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { SlashService } from './service.ts'
|
||||
import type { MenuViewInjected } from './slots.ts'
|
||||
import { MenuView } from './MenuView.tsx'
|
||||
import { en, zh, type MenuKey } from './locales.ts'
|
||||
|
||||
export { SlashService } from './service.ts'
|
||||
export { SlashController } from './controller.ts'
|
||||
export type { SlashControllerDeps, SourceRoster } from './controller.ts'
|
||||
export type { MenuViewInjected } from './slots.ts'
|
||||
export type { MenuViewProps } from './MenuView.tsx'
|
||||
export type { MenuKey } from './locales.ts'
|
||||
export type {
|
||||
ArbitrateKey, ArbitrateOutcome, BeginCommandRequest, CandidateRequest, ClientSessionContext,
|
||||
CommandClaim, ConsumeTokenRequest, InsertReferenceRequest, PickOutcome, PickVia, ReferenceCodec,
|
||||
@@ -31,7 +34,14 @@ declare module 'cordis' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Namespace owning the candidate-menu copy: group titles keyed by source name plus the pending row. */
|
||||
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface LocaleNamespaceMap {
|
||||
/** The candidate menu's copy: group titles keyed by source name, the pending row, and the listbox aria. */
|
||||
'slash.menu': MenuKey
|
||||
}
|
||||
}
|
||||
|
||||
/** Namespace owning the candidate-menu copy. */
|
||||
const MENU_NS = 'slash.menu'
|
||||
|
||||
/** Required services: controller resolution reads the session scope tree; the menu copy is localized. */
|
||||
@@ -44,13 +54,7 @@ export const inject = ['sessions', 'locale']
|
||||
*/
|
||||
export function apply(ctx: ClientContext): void {
|
||||
ctx.plugin(SlashService)
|
||||
ctx.effect(() => {
|
||||
const disposers = [
|
||||
ctx.locale.register(MENU_NS, 'zh', { command: '命令', skill: '技能', subagent: '子智能体', loading: '正在加载…' }),
|
||||
ctx.locale.register(MENU_NS, 'en', { command: 'Commands', skill: 'Skills', subagent: 'Subagents', loading: 'Loading…' }),
|
||||
]
|
||||
return () => { for (const dispose of disposers) dispose() }
|
||||
}, 'ui-slash: menu dictionaries')
|
||||
ctx.effect(() => ctx.locale.register(MENU_NS, { zh, en }), 'ui-slash: menu dictionaries')
|
||||
// Conditional mount: 'conversation.input.overlay' is declared by the
|
||||
// conversation composer entry, and the conversation service is mounted
|
||||
// after that declaration lands on the ledger — its presence is the
|
||||
@@ -62,6 +66,7 @@ export function apply(ctx: ClientContext): void {
|
||||
name: 'conversation.input.overlay',
|
||||
id: 'slash-menu',
|
||||
order: 0,
|
||||
locale: MENU_NS,
|
||||
inject: (sessionId): MenuViewInjected => {
|
||||
// Session-scoped slot: resolve this session's controller (the slot
|
||||
// frame hands ids, not ctx — the registered id→ctx interchange).
|
||||
@@ -72,7 +77,6 @@ export function apply(ctx: ClientContext): void {
|
||||
menu: controller.menu,
|
||||
onPick: (source, index) => { controller.pick(source, index) },
|
||||
onDismiss: () => { controller.dismiss() },
|
||||
t: scope.locale.bind(MENU_NS),
|
||||
}
|
||||
},
|
||||
}, MenuView), 'ui-slash: MenuView overlay registration')
|
||||
|
||||
26
packages/client/ui-slash/src/client/locales.ts
Normal file
26
packages/client/ui-slash/src/client/locales.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* `slash.menu` namespace dictionaries: group titles keyed by source name
|
||||
* (the lookup chain returns the key itself, so an unknown source shows its
|
||||
* raw name), the pending row, and the listbox aria label.
|
||||
*/
|
||||
|
||||
/** Simplified Chinese dictionary (the key-set source of truth). */
|
||||
export const zh = {
|
||||
'command': '命令',
|
||||
'skill': '技能',
|
||||
'subagent': '子智能体',
|
||||
'loading': '正在加载…',
|
||||
'suggestions.aria': '触发候选建议',
|
||||
} satisfies Record<string, string>
|
||||
|
||||
/** The slash.menu namespace key union. */
|
||||
export type MenuKey = keyof typeof zh
|
||||
|
||||
/** English dictionary, checked complete against the zh key set. */
|
||||
export const en = {
|
||||
'command': 'Commands',
|
||||
'skill': 'Skills',
|
||||
'subagent': 'Subagents',
|
||||
'loading': 'Loading…',
|
||||
'suggestions.aria': 'Trigger suggestions',
|
||||
} satisfies Record<MenuKey, string>
|
||||
@@ -9,7 +9,6 @@
|
||||
*/
|
||||
// Type-only edge: the SlotMap augmentation below merges into this package's interface.
|
||||
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { Translate } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { MenuState } from '../core/contract.ts'
|
||||
|
||||
@@ -26,7 +25,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
}
|
||||
}
|
||||
|
||||
/** Injected business face of the MenuView overlay entry. */
|
||||
/** Injected business face of the MenuView overlay entry (copy rides the standard locale seat, not this face). */
|
||||
export interface MenuViewInjected {
|
||||
/** The service's menu state store (read-only here; MenuView subscribes). */
|
||||
menu: SnapshotStore<MenuState>
|
||||
@@ -38,10 +37,4 @@ export interface MenuViewInjected {
|
||||
onPick: (source: string, index: number) => void
|
||||
/** Dismiss the menu (external pointer outside the composer area). */
|
||||
onDismiss: () => void
|
||||
/**
|
||||
* Bound translator for the menu namespace: group titles keyed by source
|
||||
* name (the locale fallback chain returns the key itself, so an unknown
|
||||
* source shows its raw name) plus the pending-row text.
|
||||
*/
|
||||
t: Translate
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ describe('apply', () => {
|
||||
await vi.waitFor(() => { expect(slots.entries('conversation.input.overlay')).toHaveLength(1) })
|
||||
const entries = slots.entries('conversation.input.overlay')
|
||||
expect(entries[0]!.options.id).toBe('slash-menu')
|
||||
// Copy rides the standard locale seat, not the business face.
|
||||
expect(entries[0]!.locale).toBe('slash.menu')
|
||||
|
||||
const slash = ctx.get('slash') as SlashService
|
||||
// StoredEntry.inject is declaration-typed ((...args: never[]) shape);
|
||||
@@ -79,8 +81,6 @@ describe('apply', () => {
|
||||
(ctx.get('sessions') as { scope(id: SessionId): Context }).scope(sid('a')),
|
||||
)
|
||||
expect(injected.menu).toBe(controller.menu)
|
||||
// The injected translator is the menu-namespace binding.
|
||||
expect(injected.t('command')).toBe('命令')
|
||||
// The pick face routes into the controller pipeline (closed menu → no-op).
|
||||
injected.onPick('command', 0)
|
||||
expect(controller.menu.getSnapshot().open).toBe(false)
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import { zh } from '../src/client/locales.ts'
|
||||
import type { MenuState, TriggerHit } from '@deepseek-ai/dsh-client-ui-slash/client'
|
||||
import { MenuView } from '../src/client/MenuView.tsx'
|
||||
|
||||
@@ -48,10 +51,10 @@ afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
// Dictionary-backed fake mirroring the LocaleService key fallback (an
|
||||
// unknown key comes back verbatim, so unknown sources show their raw name).
|
||||
const DICT: Record<string, string> = { command: 'Commands', skill: 'Skills', loading: 'Loading…' }
|
||||
const t = (key: string) => DICT[key] ?? key
|
||||
// The framework-injected t seat, stubbed over the zh dictionaries (the
|
||||
// default locale); the stub mirrors the LocaleService key fallback, so an
|
||||
// unknown source comes back verbatim (its raw name).
|
||||
const t = makeTranslate(zh, commonZh)
|
||||
|
||||
function mount(state: MenuState) {
|
||||
const menu = createSnapshotStore<MenuState>(state)
|
||||
@@ -81,7 +84,7 @@ describe('MenuView', () => {
|
||||
mount(openState())
|
||||
const options = screen.getAllByRole('option')
|
||||
expect(options.map(o => o.textContent)).toEqual(['⚑goalSet up a goal', 'plan'])
|
||||
expect(screen.queryByText('Loading…')).not.toBeNull()
|
||||
expect(screen.queryByText('正在加载…')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('titles each group with the localized source name, raw name for unknown sources, none for empty ready groups', () => {
|
||||
@@ -93,7 +96,7 @@ describe('MenuView', () => {
|
||||
{ source: 'skill', status: 'pending', items: [] },
|
||||
],
|
||||
}))
|
||||
expect(titles(view.container)).toEqual(['Commands', 'mystery', 'Skills'])
|
||||
expect(titles(view.container)).toEqual(['命令', 'mystery', '技能'])
|
||||
})
|
||||
|
||||
it('exposes the highlight via aria-activedescendant and aria-selected', () => {
|
||||
|
||||
Reference in New Issue
Block a user