Merge branch 'master' into worktree/web-skill-tool-row

This commit is contained in:
Yichen Jiang
2026-08-07 11:31:49 +08:00
committed by GitHub
244 changed files with 10810 additions and 1287 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 packages/client/ui-primitives/README.md
README.md: 385730c94831d2fd4af83f9eca0f55941551c796
README.zh.md: b8a75dbffc6549f6294dfda5988c67d6569386c9
README.md: 7571cb48424b650a1aaa5222b33a3ee14faa69b4
README.zh.md: fa0c3f24023ec8c1eb77553bfe191801b6698687

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/Input, the markdown family (MessageText/MarkdownText/JsonBlock), the read-only JsonTree inspector, the `useAnchoredMaxHeight` hook that clamps a bottom-anchored overlay to the viewport space above its anchor (re-measured on resize, scroll, and a caller-supplied dependency), TerminalBlock, DiffBlock, ReadBlock, SearchBlock, and WebBlock. Contract: api-contracts v3 §8.
Pure React atoms (zero cordis): StateDot, ic_ds_* icons, Button/Pill/Menu/Modal/Input, the OnboardingSurface first-run takeover (body-portaled mask + opaque stage that holds `#root` inert for exactly its own lifetime), the markdown family (MessageText/MarkdownText/JsonBlock), the read-only JsonTree inspector, the `useAnchoredMaxHeight` hook that clamps a bottom-anchored overlay to the viewport space above its anchor (re-measured on resize, scroll, and a caller-supplied dependency), TerminalBlock, DiffBlock, ReadBlock, SearchBlock, and WebBlock. Contract: api-contracts v3 §8.
## Hover cards

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
纯 React 原子组件(零 cordis):StateDot、ic_ds_* 图标、Button/Pill/Menu/Modal/Input、markdown 家族(MessageText/MarkdownText/JsonBlock)、只读 JsonTree 检查器、`useAnchoredMaxHeight` hook(把底部锚定的浮层高度收敛到锚点上方的视口空间,并在 resize、scroll 与调用方提供的依赖变化时重新测量)、TerminalBlock、DiffBlock、ReadBlock、SearchBlock,以及 WebBlock。契约:api-contracts v3 §8。
纯 React 原子组件(零 cordis):StateDot、ic_ds_* 图标、Button/Pill/Menu/Modal/Input、OnboardingSurface 首次使用接管层(portal 到 body 的遮罩加不透明展示层,在自身生命周期内保持 `#root` 为 `inert`)、markdown 家族(MessageText/MarkdownText/JsonBlock)、只读 JsonTree 检查器、`useAnchoredMaxHeight` hook(把底部锚定的浮层高度收敛到锚点上方的视口空间,并在 resize、scroll 与调用方提供的依赖变化时重新测量)、TerminalBlock、DiffBlock、ReadBlock、SearchBlock,以及 WebBlock。契约:api-contracts v3 §8。
## 悬浮卡片

View File

@@ -0,0 +1,29 @@
/* First-run stage: keep the product top bar visible, then let onboarding own
the complete workspace instead of presenting another settings modal. */
.onboardingOverlay {
position: fixed;
inset: 0;
z-index: 1100;
}
/* Mask */
.onboardingMask {
position: absolute;
left: 0px;
right: 0px;
top: 80px;
bottom: 0px;
background: rgba(0, 0, 0, 0.24);
/* Mask-blur */
backdrop-filter: blur(2px);
}
.onboardingStage {
position: absolute;
z-index: 1;
inset: 0;
display: flex;
justify-content: center;
overflow: hidden;
background: var(--dsw-alias-bg-layer-1);
}

View File

@@ -0,0 +1,34 @@
// OnboardingSurface: the full-viewport first-run takeover an onboarding step
// wraps its visible content in. The overlay portals to this document's body
// (the Modal precedent: ancestor stacking contexts cannot leave sticky page
// controls above the mask), and the surface holds `#root` inert for exactly
// its own lifetime — a step that renders null paints nothing and blocks
// nothing, so "should onboarding show right now" stays a plain render
// decision inside the step component.
import { useEffect } from 'react'
import type { ReactNode } from 'react'
import { createPortal } from 'react-dom'
import css from './OnboardingSurface.module.css'
/**
* Render the onboarding takeover chrome (mask + opaque stage) around one
* step's content and keep the application root inert while mounted.
* @param props.children - the step's page content, centered on the stage.
* @returns the body-portaled overlay tree.
*/
export function OnboardingSurface({ children }: { children: ReactNode }) {
useEffect(() => {
const appRoot = document.getElementById('root')
if (appRoot === null) return
appRoot.inert = true
return () => { appRoot.inert = false }
}, [])
return createPortal((
<div className={css.onboardingOverlay} role="presentation">
<div className={css.onboardingMask} aria-hidden="true" />
<div className={css.onboardingStage}>{children}</div>
</div>
), document.body)
}

View File

@@ -13,6 +13,7 @@ export type { MenuEntry, MenuItem, MenuSeparator, MenuLabel } from './Menu.tsx'
export { useAnchoredMaxHeight } from './useAnchoredMaxHeight.ts'
export { HoverCard } from './HoverCard.tsx'
export { Modal } from './Modal.tsx'
export { OnboardingSurface } from './OnboardingSurface.tsx'
export { RiskConfirmation } from './RiskConfirmation.tsx'
export type { RiskConfirmationProps } from './RiskConfirmation.tsx'
export { ConnectionBanner } from './ConnectionBanner.tsx'

View File

@@ -0,0 +1,47 @@
// @vitest-environment jsdom
import { cleanup, render } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { OnboardingSurface } from '@deepseek-ai/dsh-client-ui-primitives'
let appRoot: HTMLDivElement
beforeEach(() => {
appRoot = document.createElement('div')
appRoot.id = 'root'
document.body.appendChild(appRoot)
})
afterEach(() => {
cleanup()
appRoot.remove()
})
describe('OnboardingSurface', () => {
it('portals the overlay chrome to document.body around its content', () => {
const view = render(<OnboardingSurface><p>step content</p></OnboardingSurface>)
// Portaled: the overlay is a body child, not inside the render container.
expect(view.container.querySelector('[class*="onboardingOverlay"]')).toBeNull()
const overlay = document.body.querySelector('[class*="onboardingOverlay"]')
expect(overlay).not.toBeNull()
// The onboarding e2e pins the mask by class substring; the stage carries
// the content.
expect(overlay!.querySelector('[class*="onboardingMask"]')).not.toBeNull()
const stage = overlay!.querySelector('[class*="onboardingStage"]')
expect(stage).not.toBeNull()
expect(stage!.textContent).toBe('step content')
})
it('holds #root inert for exactly its own lifetime', () => {
const view = render(<OnboardingSurface>x</OnboardingSurface>)
expect(appRoot.inert).toBe(true)
view.unmount()
expect(appRoot.inert).toBe(false)
})
it('renders without an #root element (compositions that mount elsewhere)', () => {
appRoot.remove()
const view = render(<OnboardingSurface>x</OnboardingSurface>)
expect(document.body.querySelector('[class*="onboardingStage"]')!.textContent).toBe('x')
view.unmount()
})
})