diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 64a363a551..6d8e6d6757 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -18,8 +18,8 @@ en: d2f5701274be5110 zh: d60e2617136a6f26 /deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog: - en: 9a278223fc7a2910 - zh: f086530927d74dee + en: 0e5a5bd929dca916 + zh: bf123db2eac3dd97 /deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons: en: fff68b60f4abdc7e zh: 5cf5952ec78f7f91 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index acec371082..f5254e2090 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -46,6 +46,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `Checkbox` | Labeled native checkbox with controlled state, keyboard interaction, and disabled styling; the caller supplies localized `label` text. | | `Input` | Single-line text entry for search boxes and inline forms. Its ref targets the native input for focus and is cleared on unmount. | | `InlineEditor` | Autofocused multi-line inline editor for short drafts such as queued messages and goals. It grows with its text and its width up to six lines, then scrolls; Enter saves, Shift+Enter breaks the line, Enter during IME composition does nothing, and Escape cancels. The caller supplies the localized `label` and rebinds the scrollbar on an elevated surface. | +| `ZoomControls`, `DOCUMENT_ZOOM` | Floating document zoom controls with localized labels, optional fitting, fixed presets, and visibility/activity callbacks. Renderers share the published scale limits. | | `Menu`, `MenuItemButton` | Dropdown of `items` data rows, separators, and group labels, with nested submenus; `children` adds component rows, each a `MenuItemButton` (`separatorBefore` starts a new group), in the same list. Every row shares the styling, the keyboard walk, and the focus return; closing stays the owner's state change for both kinds. While open, ↑/↓ (with Home and End) walk the list, Tab settles the focused row, and Escape or Shift+Tab close back to the anchor; selecting a row also returns the keyboard to the anchor unless the owner moved it itself. Only a keyboard on the anchor or inside the list is intercepted, and `autoFocus` decides solely whether opening focuses the first row. | | `MenuGroup`, `observeStickyMenuGroups` | Localized, accessible groups for custom menus and listboxes, with shared sticky-heading styling and native intersection/size observation; the caller owns the observer's lifetime. | | `Pill` | Selectable capsule button for view switchers and filters; takes `active` and `onClick`. | diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index 8cd1b8cedd..35e15005fe 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -46,6 +46,7 @@ kind: "package-library" | `Checkbox` | 带标签的原生复选框,支持受控状态、键盘交互和禁用样式;调用方提供本地化的 `label` 文本。 | | `Input` | 单行文本输入,用于搜索框与行内表单。ref 指向原生输入框,供焦点控制使用,并在卸载时清空。 | | `InlineEditor` | 自动聚焦的多行行内编辑器,用于排队消息、goal 等短草稿。它随文本和宽度增高,最多六行,之后滚动;Enter 保存,Shift+Enter 换行,输入法组合中的 Enter 不生效,Escape 取消。调用方提供本地化 `label`,并在 elevated 表面上重绑滚动条。 | +| `ZoomControls`, `DOCUMENT_ZOOM` | 悬浮文档缩放控件,接收本地化标签、可选适配操作、固定预设,以及可见性和交互状态回调。渲染器共用导出的缩放范围。 | | `Menu`, `MenuItemButton` | 由 `items` 数据行、分隔线与分组标题构成的下拉菜单,支持嵌套子菜单;`children` 在同一列表中加入组件行,每行一个 `MenuItemButton`(`separatorBefore` 开启新分组)。所有行共享样式、键盘走位与焦点归还;两类行的关闭都是 owner 状态的改变。打开期间 `↑`/`↓`(以及 Home、End)在列表中走位,Tab 选定聚焦行,Escape 或 Shift+Tab 关闭并把焦点还给锚点;选定一行同样把键盘还给锚点——除非拥有者自己移动了焦点。只拦截位于锚点或列表内的键盘,`autoFocus` 仅决定打开时是否聚焦首行。 | | `MenuGroup`、`observeStickyMenuGroups` | 为自定义菜单与列表框提供本地化、可访问的分组,共用吸顶标题样式与原生交叉/尺寸观察;调用方负责观察器的生命周期。 | | `Pill` | 可选中的胶囊按钮,用于视图切换与筛选器;接受 `active` 与 `onClick`。 | diff --git a/packages/client/ui-sidebar-documentpreview/src/client/zoom/ZoomControls.module.css b/packages/client/ui-primitives/src/ZoomControls.module.css similarity index 100% rename from packages/client/ui-sidebar-documentpreview/src/client/zoom/ZoomControls.module.css rename to packages/client/ui-primitives/src/ZoomControls.module.css diff --git a/packages/client/ui-sidebar-documentpreview/src/client/zoom/ZoomControls.tsx b/packages/client/ui-primitives/src/ZoomControls.tsx similarity index 70% rename from packages/client/ui-sidebar-documentpreview/src/client/zoom/ZoomControls.tsx rename to packages/client/ui-primitives/src/ZoomControls.tsx index 16c2912d28..d99c2ab345 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/zoom/ZoomControls.tsx +++ b/packages/client/ui-primitives/src/ZoomControls.tsx @@ -1,10 +1,14 @@ /** Floating controls shared by every zoomable document renderer. */ import { forwardRef, useEffect, useImperativeHandle, useState, type ReactNode } from 'react' -import { Button, IconChevronDownOutlineRegular, IconPlusOutlineRegular, Menu, Tooltip, - type MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives' -import { MAX_FIXED_ZOOM, MIN_FIXED_ZOOM, ZOOM_OPTIONS, ZOOM_STEP, type ZoomLabels } from './types.ts' +import { Button } from './Button.tsx' +import { IconChevronDownOutlineRegular, IconPlusOutlineRegular } from './icons/index.tsx' +import { Menu, type MenuEntry } from './Menu.tsx' +import { Tooltip } from './Tooltip.tsx' +import { DOCUMENT_ZOOM, type DocumentZoomLabels } from './document-zoom.ts' import css from './ZoomControls.module.css' +const { minimum: MIN_FIXED_ZOOM, maximum: MAX_FIXED_ZOOM, step: ZOOM_STEP, presets: ZOOM_OPTIONS } = DOCUMENT_ZOOM + function steppedZoom(zoom: number, direction: -1 | 1): number { const step = direction === -1 ? Math.ceil(zoom / ZOOM_STEP) - 1 : Math.floor(zoom / ZOOM_STEP) + 1 return Math.min(MAX_FIXED_ZOOM, Math.max(MIN_FIXED_ZOOM, step * ZOOM_STEP)) @@ -17,37 +21,40 @@ export interface ZoomControlsHandle { } /** - * Present fit-width, fixed presets, and incremental controls. + * Present optional fitting, fixed presets, and incremental controls. * @param props - current resolved zoom, preference mode, labels, and callbacks. * @returns a localized zoom toolbar. */ export const ZoomControls = forwardRef void } | undefined + readonly className?: string | undefined + readonly labels: DocumentZoomLabels readonly visible: boolean readonly onZoom: (zoom: number) => void - readonly onFitWidth: () => void readonly onActiveChange: (active: boolean) => void -}>(function ZoomControls({ zoom, fitWidth, labels, visible, onZoom, onFitWidth, onActiveChange }, ref): ReactNode { +}>(function ZoomControls({ + zoom, fit, className, labels, visible, onZoom, onActiveChange, +}, ref): ReactNode { const [displayZoom, setDisplayZoom] = useState(zoom) const [open, setOpen] = useState(false) const [hovered, setHovered] = useState(false) const [focused, setFocused] = useState(false) + useEffect(() => { setDisplayZoom(zoom) }, [zoom]) useImperativeHandle(ref, () => ({ showZoom: setDisplayZoom }), []) useEffect(() => { onActiveChange(open || hovered || focused) }, [focused, hovered, onActiveChange, open]) const items: readonly MenuEntry[] = [ - { id: 'fit-width', label: labels.fitWidth }, - { type: 'separator', id: 'fit-scale' }, + ...(fit === undefined ? [] : [{ id: 'fit', label: fit.label }, { type: 'separator' as const, id: 'fit-scale' }]), ...ZOOM_OPTIONS.map(value => ({ id: String(value), label: labels.value(value * 100) })), ] - const selectedId = fitWidth ? 'fit-width' - : ZOOM_OPTIONS.includes(displayZoom as typeof ZOOM_OPTIONS[number]) ? String(displayZoom) : undefined - return
value === displayZoom) ? String(displayZoom) : undefined + return
{ if (event.pointerType !== 'touch') setHovered(true) }} - onPointerLeave={() => { setHovered(false) }} - onFocusCapture={() => { setFocused(true) }} onBlurCapture={() => { setFocused(false) }}> + onPointerEnter={(event) => { if (event.pointerType !== 'touch') setHovered(true) }} onPointerLeave={() => { setHovered(false) }} + onFocusCapture={() => { setFocused(true) }} onBlurCapture={(event) => { + if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false) + }}>