@deepseek-ai/dsh-client-ui-primitives 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +90 -24
  3. package/README.zh.md +91 -25
  4. package/lib/ConnectionIndicator.module.css +107 -0
  5. package/lib/DiffBlock.module.css +0 -6
  6. package/lib/DisclosureRow.module.css +21 -6
  7. package/lib/ReadBlock.module.css +0 -6
  8. package/lib/SearchBlock.module.css +0 -8
  9. package/lib/TerminalBlock.module.css +0 -6
  10. package/lib/Toast.module.css +7 -10
  11. package/lib/Tooltip.module.css +0 -7
  12. package/lib/WebBlock.module.css +0 -6
  13. package/lib/index.js +878 -334
  14. package/lib/markdown/CodeBlock.module.css +4 -2
  15. package/lib/markdown/MarkdownText.module.css +2 -2
  16. package/lib/types/ConnectionIndicator.d.ts +25 -0
  17. package/lib/types/DiffBlock.d.ts +28 -9
  18. package/lib/types/FishLogo.d.ts +7 -0
  19. package/lib/types/FoldToggle.d.ts +20 -0
  20. package/lib/types/HoverCard.d.ts +4 -4
  21. package/lib/types/JsonTree.d.ts +4 -6
  22. package/lib/types/Modal.d.ts +22 -18
  23. package/lib/types/OnboardingSurface.d.ts +2 -2
  24. package/lib/types/ReadBlock.d.ts +13 -1
  25. package/lib/types/ReferenceIcon.d.ts +16 -0
  26. package/lib/types/RiskConfirmation.d.ts +2 -1
  27. package/lib/types/SearchBlock.d.ts +14 -0
  28. package/lib/types/TerminalBlock.d.ts +4 -10
  29. package/lib/types/Toast.d.ts +9 -1
  30. package/lib/types/WebBlock.d.ts +13 -0
  31. package/lib/types/icons/index.d.ts +8 -0
  32. package/lib/types/index.d.ts +14 -8
  33. package/lib/types/markdown/CodeBlock.d.ts +11 -3
  34. package/lib/types/markdown/JsonBlock.d.ts +1 -1
  35. package/lib/types/markdown/MarkdownText.d.ts +8 -7
  36. package/lib/types/markdown/highlight.d.ts +44 -2
  37. package/lib/types/markdown/render.d.ts +9 -4
  38. package/lib/types/relative-time.d.ts +23 -0
  39. package/lib/types/useAnchoredPosition.d.ts +8 -6
  40. package/lib/types/useDismissOnOutsidePointer.d.ts +3 -1
  41. package/lib/types/user-text.d.ts +19 -0
  42. package/lib/user-text.module.css +35 -0
  43. package/package.json +4 -5
  44. package/lib/ConnectionBanner.module.css +0 -13
  45. package/lib/types/ConnectionBanner.d.ts +0 -12
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-primitives/README.md
5
- README.md: 7822a5d41e8125752b8bc28fea3db2232323fd81
6
- README.zh.md: 8de384f276e8be5b78d2659a1e14e4afe12c030e
5
+ README.md: 42c1110e8735dd2191c8b7a2e4dcc1f2b9b938bc
6
+ README.zh.md: 9f3c06cacebb7a596204dbe1c8e00a549ce1fcae
package/README.md CHANGED
@@ -1,44 +1,95 @@
1
+ ---
2
+ description: "Shared React UI atoms for the dsh web client: controls, icons, markdown and math rendering, and the terminal/read/diff/search/web output cards (zero cordis)."
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-client-ui-primitives
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- Pure React atoms (zero cordis): StateDot, DisclosureRow, ic_ds_* icons, Button/Pill/Menu/Modal/Input, the Toast transient banner, 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), the `useAnchoredPosition` hook that holds a fixed-position floating panel under its anchor (measure, offset, clamp inside the viewport margin, re-placed on capture-phase scroll, window resize, and the panel's own size changes), TerminalBlock, DiffBlock, ReadBlock, SearchBlock, and WebBlock.
10
+ ## Summary
11
+
12
+ `dsh-client-ui-primitives` is the web client's shared React component library: every feature plugin composes its UI from these atoms, and nothing here depends on Cordis or the slot system. It provides the control set (buttons, pills, inputs, menus, modals, toast banners, disclosure rows, hover cards, connection indicators), the icon glyphs and brand marks, positioning hooks for anchored overlays, and the content renderers for agent output: markdown with TeX math, terminal output, file reads, diffs, search results, web retrieval, and JSON inspection. The renderers are built for untrusted model output — raw HTML is dropped, links are neutralized or opened safely, and ANSI escape sequences are parsed rather than passed through. User-facing copy is supplied through label props; the feature plugin that composes an atom owns localization.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Compose feature UI from these atoms whenever the web client needs a standard control or an agent-output renderer. They render through React only and take `--dsw-*` design tokens from the theme, so they fit any plugin without importing the theme or the slot system.
29
+
30
+ ### Controls and icons
31
+
32
+ `Button`, `Pill`, `Input`, `Menu`, `Modal`, `Tooltip`, `DisclosureRow`, `StateDot`, `HoverCard`, `Toast`, `ConnectionIndicator`, `RiskConfirmation`, and the `OnboardingSurface` first-run takeover cover the common interaction shapes. The `ic_ds_*` icon set and `FishLogo`/`BrandWordmark` marks fill brand and inline-icon slots. `ConnectionIndicator` renders a warning-colored disconnected action, a connecting label whose one-to-three dots advance every 500ms independently of retry timing, or a success-colored recovered status. Every state reserves the widest supplied label and uses fixed icon and text columns, so copy changes do not move or resize the control. Its owner supplies visibility, the recovery hold, localized labels, and the immediate-reconnect callback; the primitive uses no native title tooltip. `useAnchoredPosition` and `useAnchoredMaxHeight` keep floating panels and bottom-anchored overlays clamped to the viewport and following their anchor. `HoverCard` keeps its portaled preview reachable across the anchor gap and can expose a copy button through the `copyText` prop. `Toast` holds for the window its owner names through `holdMs`, because how long a banner has to stay depends on how much there is to read; the same value drives its unmount timer and the stylesheet's fade delay, so the two cannot disagree.
33
+
34
+ ### Rendering agent output
6
35
 
7
- ## Hover cards
36
+ `MarkdownText` renders untrusted GFM and TeX math, blocks unsafe links and images, and can turn resolved file mentions into explicit controls. While a reply streams, it freezes completed blocks and highlights a growing fence from saved Shiki grammar state; the final render uses the same span tree ([incremental renderer](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md), [streaming fence highlighting](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.md)). `TerminalBlock`, `ReadBlock`, `DiffBlock`, `SearchBlock`, and `WebBlock` render the matching tool-result intent with copy controls, overflow handling, and ANSI processing where applicable. `JsonTree` and `JsonBlock` inspect JSON values read-only, while `MessageText` remains the literal-text primitive for user-authored content.
8
37
 
9
- `HoverCard` keeps its portaled preview reachable across the anchor gap with a pointer-leave grace. A consumer may also pass `copyText`: the card then exposes button semantics for pointer and keyboard activation, includes that value after the `copyLabel` prefix in its accessible name, writes the exact value through the package clipboard helper, and temporarily replaces its content with `copiedLabel` only after the host accepts the write. A non-collapsed text selection intersecting the card suppresses pointer-click activation, while success feedback retains the original card height and clears when the card closes or after one second. `copyLabel` and `copiedLabel` are label props because this zero-cordis atom cannot read the application locale; omitting `copyText` preserves the read/select-only card. Historical rationale: [the archived hover-card copy note](../../../.agents/notes/archived/feature/2026-07-31-hover-card-click-copy.md).
38
+ ### Localizing copy
10
39
 
11
- ## Toast
40
+ The atoms cannot read the application locale, so every piece of user-facing copy arrives through required label props. `HoverCard`, `TerminalBlock`, `JsonTree`, `CodeBlock`, `MarkdownText`, `JsonBlock`, `ConnectionIndicator`, `Modal`, `DiffBlock`, `ReadBlock`, `SearchBlock`, and `WebBlock` accept complete localized labels. The package owns no language fallback; omission fails typechecking, and each feature maps its typed `t` seat into the primitive's label interface.
12
41
 
13
- `Toast` is the transient top banner: it slides in, holds at full opacity for three seconds, fades over one second, then calls `onDone` so the owner can unmount it. It renders `role="alert"` with an optional leading icon slot and takes its copy as a required prop (zero-cordis: the owner localizes). It body-portals with `pointer-events: none`, sits 120px from the viewport top, and centers horizontally over the optional `anchor` element (re-measured on window resizes) — the composer passes its card so the banner centers over the chat column rather than the whole window — falling back to the viewport center without one. Re-showing the same message requires a remount — owners key the element by a per-show sequence so an identical repeated message restarts the hold-and-fade cycle instead of silently reusing the faded banner. Under `prefers-reduced-motion: reduce` the slide-in is dropped and only the delayed fade remains. It layers above the ui-attachment image lightbox so a failure reported during a preview stays readable.
42
+ -----
14
43
 
15
- ## Markdown rendering
44
+ <a id="understand-the-implementation"></a>
45
+ ## Understand the implementation
16
46
 
17
- `MarkdownText` renders GFM and `$…$`, `$$…$$`, `\(…\)`, and `\[…\]` TeX math from untrusted assistant output through React elements, with math typeset by KaTeX and trusted commands disabled; block-level same-line `$$…$$` is display math, including `\tag{}`. A narrow micromark extension lets asterisk strong emphasis ending in punctuation close before adjacent CJK text, where prose normally omits the whitespace CommonMark requires; single-asterisk emphasis, non-CJK adjacency, escapes, code, and math retain upstream parsing. It omits raw HTML, neutralizes relative and non-HTTP(S)/mailto links, opens HTTP(S) links with safe external-link attributes, and renders absolute HTTP(S) images without a referrer; relative paths, absolute local paths, `file:` URLs, and unsupported schemes retain their alt text. Inline code whose complete value is an absolute HTTP(S) URL keeps its code styling and gains the same safe external anchor; commands, partial URLs, other schemes, and fenced code remain inert. An optional `fileMentions` resolver lets the owning view link inline code that names a real file: the token keeps code styling and gains a button wired to the resolved opener, with the resolver's accessible label and full-path `title`. The renderer never guesses at what looks like a path — an unresolved token stays inert, mentions apply to settled renders only (the streaming cache must not bake in handlers that could go stale), and a token inside an anchor stays inert because a button cannot nest there. While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail behind them re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply ([mechanism and DOM-parity contract](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md)). Tables size by column count (deepsuite chat parity): under four columns — or inside a blockquote — a table fills its column and wraps cell text down to the cells' minimum readable width, while four-or-more-column tables keep their natural width, scroll horizontally inside their wrapper, and carry the stable `md-table-wide` class so a hosting layout can widen the wrapper past its column (the chat transcript's container-query breakout in `dsh-client-ui-conversation`); a wide table's horizontal bar reveals on hover or keyboard focus (the wrapper carries `tabindex="0"`) instead of staying painted ([decision record](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.md)). `MessageText` remains the literal-text primitive for user-authored content. `extractMarkdownPlainText` removes Markdown presentation markup for compact labels while preserving raw HTML as literal text. Element spacing, responsive images, tables, links, and inline code use the same `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` tokens as deepsuite `@deepseek/md`. Fenced blocks render through `CodeBlock` (language banner, copy control, shiki for the registered grammars).
47
+ <details>
48
+ <summary>Implementation internals — click to expand</summary>
18
49
 
19
- ## Terminal output
50
+ The package is one separation: presentational React atoms with zero Cordis and zero slot knowledge, styled only through `--dsw-*` tokens, while every feature-specific concern (locale, session data, composition) stays in the composing plugin.
20
51
 
21
- `TerminalBlock` renders a shell command as a terminal surface: one prompt row per line of the command (the shortened `cwd` label on the first row only, since the view knows one working directory and a `cd` moves later lines elsewhere, then that line), the command's output, a status pill for a non-zero exit code or a terminating signal, and a copy control that writes the raw `output` prop. A run-state `StateDot` marks the call once, on the first row, out of flow in a gutter the card reserves as its own left padding, so the dot sits inside the card box yet left of the prompt text. It reaches three of `StateDot`'s states — the chase while `running`, red for the same exit status that renders the pill, green otherwise — so a card states whether its command is still running rather than leaving that to be inferred from the presence of output; it carries one visually hidden text label because `StateDot` is `aria-hidden`. One dot regardless of line count is deliberate: the exit status is the whole call's, so a dot per line would claim a per-line outcome the view does not carry. Command text is `white-space: pre`, so repeated spaces, tabs, and an indented continuation render verbatim while the row stays single-line and ellipsizes. ANSI escape sequences are parsed with `anser` (bundled into this package's browser artifact) into React spans; cursor movements replay into a per-line column buffer before inert controls are stripped, since carriage return and backspace only MOVE the cursor: `100%` + CR + `OK` alone shows `OK0%`, while the `\x1b[K` a spinner writes with its redraw erases the tail so `100%\r\x1b[KOK` shows `OK`. Erase-in-line is honored in all three parameter forms, the cursor advances by terminal columns (8-column tab stops, two for emoji and CJK, none for a combining mark), and SGR state is normalized per cell as a terminal stores it, threading across lines and closing at the state the line ended in; basic-16 foreground colors map onto `--dsw-*` tokens, while 256-palette and truecolor values pass through as literal rgb. Output keeps `white-space: pre` with horizontal scrolling, so column-aligned output holds its alignment instead of soft-wrapping, and collapses to a head slice plus a tail slice past `maxLines` (default 16) behind an expand button. Rationale: [the web terminal card note](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.md).
52
+ ### Source map
22
53
 
23
- ## Read rendering
54
+ | File | Role |
55
+ |---|---|
56
+ | [`src/index.ts`](src/index.ts) | Public atom exports |
57
+ | [`src/markdown/`](src/markdown/) | Markdown and math pipeline: micromark parsing, KaTeX typesetting, incremental streaming renderer, `CodeBlock`/`JsonBlock` |
58
+ | [`src/TerminalBlock.tsx`](src/TerminalBlock.tsx) | ANSI escape parsing (`anser`) and terminal card rendering |
59
+ | [`src/ReadBlock.tsx`](src/ReadBlock.tsx) / [`src/DiffBlock.tsx`](src/DiffBlock.tsx) | Read and diff cards |
60
+ | [`src/SearchBlock.tsx`](src/SearchBlock.tsx) / [`src/WebBlock.tsx`](src/WebBlock.tsx) | Search and web-retrieval cards |
61
+ | [`src/icons/`](src/icons/) | `ic_ds_*` glyph components and brand marks |
62
+ | [`src/useAnchoredPosition.ts`](src/useAnchoredPosition.ts) / [`src/useAnchoredMaxHeight.ts`](src/useAnchoredMaxHeight.ts) | Floating-panel and overlay geometry hooks |
24
63
 
25
- `ReadBlock` renders a returned file window as a line-numbered, syntax-highlighted code surface: a bold path (or presenter-supplied title) banner with a copy control, then the content lines with their file line numbers in a gutter (a windowed read keeps the file's own numbering, so a read past an offset starts above 1). A `totalLines` exceeding the window count draws a `showing N of M` note, and the body collapses to a head slice plus a tail slice past `maxLines` (default 16, the TerminalBlock split arithmetic) behind an expand button. Highlighting runs through the same shiki path as `CodeBlock`. Rationale: [the web read card note](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card.md).
64
+ ### Streaming markdown
26
65
 
27
- ## Diff rendering
66
+ While a reply streams, `MarkdownText` parses incrementally: all but the trailing two blocks freeze as cached React elements and only the source tail re-parses per chunk, so per-chunk work tracks the tail instead of the whole reply. A growing fenced block tokenizes completed text from saved Shiki grammar state plus the unfinished last line; completed lines retain their DOM, and the settled render uses the same span tree. The settled full parse at finalize also resolves references that crossed the freeze boundary ([incremental renderer](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.md), [streaming fence highlighting](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.md)).
28
67
 
29
- `DiffBlock` renders a file mutation as an inline diff surface: one bold path header per file, the removed lines (`- `, error token) above the added lines (`+ `, success token), a `⋯` gap before a same-file second hunk, and a dim `└ +A -R · N file(s)` footer. Lines are `white-space: pre` with horizontal scrolling, so a source line holds its indentation instead of soft-wrapping, and the body collapses to a head slice plus a tail slice past `maxLines` (default 16, `TerminalBlock`'s split arithmetic) behind an expand button. A create (`oldText: null`) has no removed side. The copy control writes the prefixed diff text (path headers, `- `/`+ ` lines, the gap) so a multi-file copy stays attributable, and floats in the top-right corner rather than on a banner row of its own. Geometry mirrors `CodeBlock`/`TerminalBlock`. Rationale: [the web diff card note](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.md).
68
+ ### Geometry and overflow
30
69
 
31
- ## Search results
70
+ The output cards share one geometry model: `white-space: pre` with horizontal scrolling so column-aligned content keeps its alignment, and a head-plus-tail slice behind an expand button past `maxLines` (default 16) so a long body never stretches the card. `TerminalBlock` parses ANSI into React spans with a per-line column buffer for cursor movement, honoring erase-in-line, tab stops, and character width.
32
71
 
33
- `SearchBlock` renders a completed search, one component for both kinds (discriminated by `kind`). A `matches` (grep) shows each file as a bold path header with its `lineNumber: line` rows, the per-file group collapsible; a `paths` (glob) shows a flat path list. Both flatten to one row list the height cap slices head/tail over (default 16, the TerminalBlock split arithmetic), and neither soft-wraps — a long match line or path scrolls horizontally instead of folding. The banner summary folds the pre-cap total in when the tool capped the result (`显示 X / 共 N 处匹配 · K 个文件` for grep, `显示 X / 共 N 个路径` for glob), so the card never presents a capped result as complete; a copy control writes the whole structured result regardless of the cap or which groups are collapsed. Geometry mirrors CodeBlock/TerminalBlock. Rationale: [the web search card note](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.md).
72
+ </details>
34
73
 
35
- ## Web retrieval
74
+ -----
36
75
 
37
- `WebBlock` renders a completed web retrieval, one component for both kinds of the `web` render intent (discriminated by `kind`). A `search` shows an optional provider answer (through `MarkdownText`) above an ordered citation list: each source is a safe external link labelled by its title, or its hostname, falling back to the raw URL when the URL does not parse or has no hostname (a `file:`/`data:` URL) so a label is never blank; its snippet and publication date render below it. Only http(s) URLs become anchors (`target`/`rel` set) — the http(s) subset of the allowlist `MarkdownText` applies to untrusted links (it also permits `mailto:`, excluded here); any other URL renders as plain text. The whole list renders in one fixed-height scroll container (`max-height: 320px`, `overflow-y: auto`), so a list taller than that scrolls vertically in place instead of growing the card; `<li value>` pins each source's citation number, contiguous from 1, rather than leaving it to the `<ol>`'s implicit count. When a search legitimately returns no answer and no sources, the card shows an explicit empty-state note rather than a blank `<ol>` (the chat row does not surface the raw result content). A `fetch` shows a compact summary: the linked final URL and its HTTP status. Both mark a capped retrieval. Rationale: [the web result card note](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.md) and [the source scroll note](../../../.agents/notes/implemented/feature/2026-08-03-web-search-source-scroll.md).
76
+ <a id="further-exploration"></a>
77
+ ## Further Exploration
38
78
 
79
+ These pages place the atoms in the client stack and the design system.
80
+
81
+ - [ui-renderer](../ui-renderer/README.md) — the React renderer that mounts the assembled application and binds slot data.
82
+ - [ui-tool](../ui-tool/README.md) — the tool-call presentation layer that composes these output cards.
83
+ - [ui-conversation](../ui-conversation/README.md) — the chat surface that renders markdown replies and tool cards.
84
+ - [ui-theme](../ui-theme/README.md) — the `--dsw-*` token system these atoms style through.
85
+ - [Web styling](../../../docs/web-styling.md) — the authoritative styling rules for web client components.
86
+
87
+ -----
88
+
89
+ <a id="model-experience"></a>
39
90
  ## Model Experience
40
91
 
41
- None, as the package renders pure React atoms in the browser; nothing here reaches a model request.
92
+ None, as the package is a browser-side UI plugin layer that registers nothing model-facing.
42
93
 
43
94
  #### KV Cache effect
44
95
 
@@ -46,9 +97,24 @@ None; this package neither assembles nor sends a provider request.
46
97
 
47
98
  ## Known Limitations and Deferred Work
48
99
 
49
- - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. Inline links and references resolved within one parse are unaffected.
50
- - **Glyph-level icons are redrawn approximations** — the fish logo (and the sparkle held by ui-conversation) come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists.
51
- - **Pill and Input have no design source** — both atoms are self-defined; the sidebar search field and view-tab strip that resemble them are consumer-owned compositions, not these atoms.
52
- - **No `Active` StateDot variant** the supported states are done, warning, ongoing, and error.
53
- - **User-facing copy localizes through label props, defaulting to the original Chinese literals** — the atoms are zero-cordis and cannot reach `ctx.locale`, so `HoverCard` (`copyLabel`/`copiedLabel`), `TerminalBlock` (`labels`), `JsonTree` (`labels`), `CodeBlock` (`copyLabel`/`copiedLabel`), `MarkdownText` (`codeLabels`), `JsonBlock` (`truncatedLabel`), `ConnectionBanner` (`label`), and `Modal` (`closeLabel`) take their copy as optional props. Localized plugins pass dictionary-driven labels from their own `t` seat; a consumer that passes nothing gets those defaults. `WebBlock` does not yet follow this pattern: its source-list and fetch truncation notes and its empty-search note stay inline Chinese, pending the same label-prop treatment.
54
- - **`TerminalBlock` is not a terminal emulator** — it renders settled or still-running command output, not an interactive session: SGR color and attributes are honored, and so are the in-line cursor movements a progress line uses carriage return, backspace, erase-in-line, tab stops and character width. Absolute cursor positioning, screen clearing, and alternate-screen sequences are stripped. Basic-16 magenta and cyan have no token equivalent and stay literal rgb.
100
+ <a id="known-limitations-and-deferred-work"></a>
101
+
102
+
103
+ These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap.
104
+
105
+ - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it.
106
+ - **Glyph-level icons are redrawn approximations** — the fish logo and the sparkle mark come from font glyphs whose vector geometry is not exportable from the local design data; hand-authored recreations stand in until an exact export path exists.
107
+ - **`Pill` and `Input` have no design source** — both atoms are self-defined; the sidebar search field and view-tab strip that resemble them are consumer-owned compositions, not these atoms.
108
+ - **No `Active` `StateDot` variant** — the supported states are done, warning, ongoing, and error.
109
+ - **User-facing copy is required at the render site** — the atoms are zero-Cordis and cannot reach `ctx.locale`; each feature must supply complete localized labels through the primitive's typed props ([decision](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.md)).
110
+ - **`TerminalBlock` is not a terminal emulator** — it renders settled or still-running command output, not an interactive session: SGR colors, carriage return, backspace, erase-in-line, tab stops, and character width are honored; absolute cursor positioning, screen clearing, and alternate-screen sequences are stripped.
111
+
112
+ <a id="dev-note"></a>
113
+ ### Dev Note
114
+
115
+ <details>
116
+ <summary>Working context for maintainers — click to expand</summary>
117
+
118
+ None.
119
+
120
+ </details>
package/README.zh.md CHANGED
@@ -1,54 +1,120 @@
1
+ ---
2
+ description: "dsh Web 客户端共享的 React UI 原子组件:控件、图标、Markdown 与数学公式渲染,以及终端/读取/差异/搜索/网页输出卡片(零 cordis)。"
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-client-ui-primitives
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- React 原子组件(零 cordis):StateDot、DisclosureRow、ic_ds_* 图标、Button/Pill/Menu/Modal/Input、Toast 短时横幅、OnboardingSurface 首次使用接管层(portal 到 body 的遮罩加不透明展示层,在且仅在自身生命周期内保持 `#root` 为 `inert`)、markdown 家族(MessageText/MarkdownText/JsonBlock)、只读 JsonTree 检查器、`useAnchoredMaxHeight` 钩子(把底部锚定的浮层高度收敛到锚点上方的视口空间,并在 resize、scroll 与调用方提供的依赖变化时重新测量)、`useAnchoredPosition` 钩子(让固定定位的浮动面板跟住锚点:测量、偏移、按视口边距钳制,并在捕获阶段滚动、窗口缩放与面板自身尺寸变化时重新定位)、TerminalBlock、DiffBlock、ReadBlock、SearchBlock,以及 WebBlock。
10
+ ## 概述
11
+
12
+ `dsh-client-ui-primitives` 是 Web 客户端共享的 React 组件库:每个功能插件都用这些原子组件拼装自己的 UI,而这里没有任何内容依赖 Cordis 或 slot 系统。它提供控件集(按钮、胶囊、输入框、菜单、模态框、Toast 横幅、折叠行、悬浮卡片、连接指示器)、图标字形与品牌标记、锚定浮层用的定位钩子,以及 agent 输出的内容渲染器:带 TeX 公式的 markdown、终端输出、文件读取、差异、搜索结果、网页检索与 JSON 检查。这些渲染器为不受信任的模型输出而设计——原始 HTML 会被丢弃、链接会被失效或安全打开、ANSI 转义序列会被解析而非透传。面向用户的文案通过 label prop 提供;拼装某个原子组件的功能插件负责本地化。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 只要 Web 客户端需要标准控件或 agent 输出渲染器,就用这些原子组件拼装功能 UI。它们只经 React 渲染,并从主题取得 `--dsw-*` 设计 token,因此无需导入主题或 slot 系统即可适配任意插件。
29
+
30
+ ### 控件与图标
31
+
32
+ `Button`、`Pill`、`Input`、`Menu`、`Modal`、`Tooltip`、`DisclosureRow`、`StateDot`、`HoverCard`、`Toast`、`ConnectionIndicator`、`RiskConfirmation` 与首次运行接管层 `OnboardingSurface` 覆盖常见的交互形态。`ic_ds_*` 图标集与 `FishLogo`/`BrandWordmark` 标记填充品牌与行内图标 slot。`ConnectionIndicator` 可渲染警告色的断联操作、以独立于 retry 时序的 500ms 节奏推进一至三个点的连接中状态,或成功色的恢复状态。所有状态都为最长的输入 label 预留空间,并使用固定的图标列和文字列,因此文案变化不会移动控件或改变其宽度。它的 owner 提供可见性、恢复驻留时间、本地化 label 与立即重连回调;该原语不使用原生 title tooltip。`useAnchoredPosition` 与 `useAnchoredMaxHeight` 让浮动面板与底部锚定浮层始终钳制在视口内并跟随锚点。`HoverCard` 通过指针离开宽限期让采用 portal 的预览在跨过锚点间隙时仍可触及,并可通过 `copyText` prop 提供复制按钮。 `Toast` 的停留时长由使用方通过 `holdMs` 指定,因为横幅该留多久取决于有多少内容要读;同一个值同时驱动它的卸载定时器与样式表的淡出延迟,两者不可能再错位。
33
+
34
+ ### 渲染 agent 输出
6
35
 
7
- ## 悬浮卡片
36
+ `MarkdownText` 渲染不可信的 GFM 与 TeX 公式、阻止不安全的链接与图片,并可把已解析的文件提及转换为显式控件。回复流式输出时,它冻结已完成的块,并从保存的 Shiki grammar state 为不断增长的 fence 增量高亮;最终渲染使用相同的 span 树([增量渲染器](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md)、[流式 fence 高亮](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.zh.md))。`TerminalBlock`、`ReadBlock`、`DiffBlock`、`SearchBlock` 与 `WebBlock` 把对应的工具结果意图渲染为带复制控件、溢出处理及适用时 ANSI 处理的卡片。`JsonTree` 与 `JsonBlock` 以只读方式检查 JSON 值;`MessageText` 仍是用户创作内容的字面文本原语。
8
37
 
9
- `HoverCard` 通过指针离开宽限期,使采用 portal 渲染的预览在跨过与锚点之间的间隙时仍可触及。消费方还可传入 `copyText`:此时卡片为指针与键盘激活提供按钮语义,其无障碍名称会在 `copyLabel` 前缀后包含该值,通过包内剪贴板辅助函数原样写入该值,并且只有宿主接受写入后,才会临时将内容替换为 `copiedLabel`。与卡片相交的非折叠文本选区会阻止指针点击激活;成功反馈保持卡片原有高度,并随卡片关闭或在一秒后清除。`copyLabel` 和 `copiedLabel` 采用 label prop,是因为这个 zero-cordis 原子组件无法读取应用 locale;省略 `copyText` 时,卡片维持只读且可选择文本的行为。历史依据见[已归档的悬浮卡片复制 Agent Note](../../../.agents/notes/archived/feature/2026-07-31-hover-card-click-copy.md)。
38
+ ### 本地化文案
10
39
 
11
- ## Toast
40
+ 这些原子组件无法读取应用 locale,因此每段面向用户的文案都必须通过 label prop 提供。`HoverCard`、`TerminalBlock`、`JsonTree`、`CodeBlock`、`MarkdownText`、`JsonBlock`、`ConnectionIndicator`、`Modal`、`DiffBlock`、`ReadBlock`、`SearchBlock` 与 `WebBlock` 接收完整的本地化 label。本包不拥有语言回退;遗漏会导致类型检查失败,各功能会把带类型的 `t` 席位映射到 primitive 的 label 接口。
12
41
 
13
- `Toast` 是顶部的短时横幅:滑入后满不透明度停留三秒,再用一秒淡出,随后调用 `onDone` 由持有方卸载。它渲染 `role="alert"`,带可选的前置图标插槽,文案是必填 prop(零 cordis,由持有方本地化)。它经 body portal 渲染且 `pointer-events: none`,距视口顶部 120px,水平中心跟随可选的 `anchor` 元素(窗口尺寸变化时重测)——composer 传入自己的卡片,横幅因此在聊天列而非整个窗口上居中——不传则回退到视口居中。重复展示同一条消息需要重新挂载,持有方用每次展示递增的序号作为 key,让相同文案重新走完停留与淡出,而不是静默复用已淡出的横幅。`prefers-reduced-motion: reduce` 下去掉滑入,只保留延迟淡出。它的层级高于 ui-attachment 的图片灯箱,预览打开时报出的失败仍然可读。
42
+ -----
14
43
 
15
- ## Markdown 渲染
44
+ <a id="understand-the-implementation"></a>
45
+ ## 理解实现
16
46
 
17
- `MarkdownText` 通过 React 元素渲染来自不受信任 assistant 输出的 GFM 与 `$…$`、`$$…$$`、`\(…\)` 和 `\[…\]` TeX 公式,公式由 KaTeX 排版并禁用受信任命令;块级同一行 `$$…$$` 是显示公式并支持 `\tag{}`。一个小范围的 micromark 扩展允许由星号标记、以标点结尾的粗体在紧邻的 CJK 文本前闭合,以适应 CJK 文本通常省略 CommonMark 所要求空格的写法;单星号强调、紧邻非 CJK 文本的情况、转义、代码与数学公式仍沿用上游解析行为。它会省略原始 HTML,使相对链接及非 HTTP(S)/mailto 链接失效,以安全的外部链接属性打开 HTTP(S) 链接,并在不发送 referrer 的情况下渲染采用绝对 HTTP(S) URL 的图片;相对路径、绝对本地路径、`file:` URL 与不受支持的 scheme 会保留其 alt 文本。完整内容为绝对 HTTP(S) URL 的行内代码会保留代码样式,并获得同样安全的外部链接;命令、非完整 URL、其他 scheme 与围栏代码仍不会成为链接。可选的 `fileMentions` 解析器让持有该组件的视图为命名真实文件的行内代码添加可点击入口:token 保留代码样式,并获得一个连接到解析所得 opener 的按钮,按钮带有解析器提供的无障碍标签和以完整路径为值的 `title`。渲染器绝不猜测哪些内容像路径:未解析的 token 保持不可交互;文件提及仅应用于已定稿的渲染(流式缓存不得固化可能过期的 handler);锚点内的 token 也保持不可交互,因为按钮不能嵌套其中。回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复([机制与 DOM 一致性约定](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md))。表格按列数决定尺寸(对齐 deepsuite chat):不足四列——或位于 blockquote 内——的表格填满所在列,单元格文本换行收缩至最小可读列宽;四列及以上的表格保持自然宽度、在包裹层内横向滚动,并携带稳定的 `md-table-wide` 类,供宿主布局把包裹层加宽到所在列之外(`dsh-client-ui-conversation` 中聊天转录区的容器查询突破样式);宽表的横向滚动条在悬停或键盘聚焦(包裹层带 `tabindex="0"`)时才出现、不再常驻([决策记录](../../../.agents/notes/implemented/feature/2026-08-19-web-markdown-wide-table-view.zh.md))。`MessageText` 仍是用户创作内容使用的字面文本原语。`extractMarkdownPlainText` 会移除 Markdown 呈现标记以用于紧凑标签,同时将原始 HTML 保留为字面文本。元素间距、响应式图片、表格、链接与行内代码使用与 deepsuite `@deepseek/md` 相同的 `--dsw-alias-markdown-*` / `--dsw-font-markdown-*` token。围栏代码块通过 `CodeBlock` 渲染(语言横幅、复制控件,以及对已注册语法使用 shiki)。
47
+ <details>
48
+ <summary>实现细节——点击展开</summary>
18
49
 
19
- ## 终端输出
50
+ 本包只做一件事:提供零 cordis、零 slot 知识、仅经 `--dsw-*` token 设置样式的纯 React 原子组件,而所有功能专属的关注点(locale、会话数据、组合)都留在拼装它们的插件中。
20
51
 
21
- `TerminalBlock` 将一条 shell 命令渲染为终端表层:命令的每一行各占一个提示行(缩短后的 `cwd` 标签只出现在第一行,因为视图只知道一个工作目录,而一个 `cd` 就会让后面的行去到别处,标签之后是该行)、命令输出、非零退出码或终止信号对应的状态胶囊,以及写入原始 `output` prop 的复制控件。一枚运行状态 `StateDot` 为整次调用标记一次,位于第一行,以脱离文档流的方式落在卡片以自身左内边距预留的落区中,因此它位于卡片盒之内、提示文字之左。它用到 `StateDot` 的三种状态——`running` 期间为追逐动画,与渲染状态胶囊相同的退出状态为红色,其余为绿色——因此卡片直接陈述其命令是否仍在运行,而不是让人从有无输出中推断;由于 `StateDot` 是 `aria-hidden`,它携带一处视觉隐藏的文本标签。无论多少行都只有一枚状态点是有意为之:退出状态属于整次调用,因此每行一枚就会声称一个视图并不携带的逐行结果。命令文本使用 `white-space: pre`,因此重复空格、制表符与缩进续行都原样呈现,同时该行仍保持单行并以省略号截断。ANSI 转义序列通过 `anser`(打进本包浏览器产物)解析为 React span;光标移动在剥除无显示意义控制符之前先重放进逐行的列缓冲,因为回车与退格**只移动**光标:单是 `100%` 加回车再加 `OK` 显示为 `OK0%`,而 spinner 随重绘写出的 `\x1b[K` 会擦掉尾巴,因此 `100%\r\x1b[KOK` 显示为 `OK`。行内擦除的三种参数形式都被遵循,光标按终端列推进(8 列制表位;emoji 与 CJK 占两列;组合标记不占列),SGR 状态按单元格归一化存储,与终端一致,并跨行延续、在行结束时的状态处收束;基础 16 色前景色映射到 `--dsw-*` token,而 256 色板与真彩色值按字面 rgb 透传。输出保持 `white-space: pre` 并支持横向滚动,因此按列对齐的输出保留其对齐而不会软换行;超过 `maxLines`(默认 16)时折叠为头部切片加尾部切片,由展开按钮控制。原理:[Web 终端卡片笔记](../../../.agents/notes/implemented/feature/2026-07-28-web-terminal-card.zh.md)。
52
+ ### 源码地图
22
53
 
23
- ## Read 渲染
54
+ | 文件 | 职责 |
55
+ |---|---|
56
+ | [`src/index.ts`](src/index.ts) | 原子组件公开导出 |
57
+ | [`src/markdown/`](src/markdown/) | Markdown 与数学公式流水线:micromark 解析、KaTeX 排版、增量流式渲染器、`CodeBlock`/`JsonBlock` |
58
+ | [`src/TerminalBlock.tsx`](src/TerminalBlock.tsx) | ANSI 转义解析(`anser`)与终端卡片渲染 |
59
+ | [`src/ReadBlock.tsx`](src/ReadBlock.tsx) / [`src/DiffBlock.tsx`](src/DiffBlock.tsx) | 读取与差异卡片 |
60
+ | [`src/SearchBlock.tsx`](src/SearchBlock.tsx) / [`src/WebBlock.tsx`](src/WebBlock.tsx) | 搜索与网页检索卡片 |
61
+ | [`src/icons/`](src/icons/) | `ic_ds_*` 字形组件与品牌标记 |
62
+ | [`src/useAnchoredPosition.ts`](src/useAnchoredPosition.ts) / [`src/useAnchoredMaxHeight.ts`](src/useAnchoredMaxHeight.ts) | 浮动面板与浮层几何钩子 |
24
63
 
25
- `ReadBlock` 将返回的文件窗口渲染为带行号、语法高亮的代码表层:一个粗体路径(或 presenter 提供的标题)横幅加复制控件,其下是内容行,行号槽里是文件自身的行号(窗口化的 read 保留文件本身的编号,因此偏移之后的 read 从大于 1 处起始)。`totalLines` 超过窗口行数时画出 `showing N of M` 提示;超过 `maxLines`(默认 16,与 TerminalBlock 相同的切分算法)时折叠为头部切片加尾部切片,由展开按钮控制。高亮走与 `CodeBlock` 相同的 shiki 路径。原理:[Web read 卡片笔记](../../../.agents/notes/implemented/feature/2026-07-30-web-read-card.zh.md)。
64
+ ### 流式 markdown
26
65
 
27
- ## Diff 渲染
66
+ 回复流式输出期间,`MarkdownText` 增量解析:除末尾两个块外全部冻结为缓存的 React 元素,每个分片只重新解析其后的源文本尾部,因此每分片的工作量跟随尾部而非整个回复。不断增长的 fenced block 会从已保存的 Shiki grammar state 加上尚未完成的最后一行继续分词;已完成行保留其 DOM,定稿渲染则使用相同的 span 树。定稿时的全量解析还会解析跨过冻结边界的引用([增量渲染器](../../../.agents/notes/implemented/architecture/2026-08-06-web-markdown-incremental-ast-renderer.zh.md)、[流式 fence 高亮](../../../.agents/notes/implemented/feature/2026-08-20-web-streaming-fence-highlight.zh.md))。
28
67
 
29
- `DiffBlock` 将一次文件改动渲染为内联 diff 表层:每个文件一个粗体路径头、删除行(`- `,error token)在新增行(`+ `,success token)之上、同文件第二个 hunk 前一个 `⋯` gap,以及暗色 `└ +A -R · N file(s)` 页脚。各行使用 `white-space: pre` 并横向滚动,因此源码行保留其缩进而不软换行;超过 `maxLines`(默认 16,与 `TerminalBlock` 相同的切分算法)时折叠为头部切片加尾部切片,由展开按钮控制。新建(`oldText: null`)没有删除侧。复制控件写入带前缀的 diff 文本(路径头、`- `/`+ ` 行、gap),使多文件复制保持可归属,并浮在右上角而非占据自己的 banner 行。几何结构与 `CodeBlock`/`TerminalBlock` 一致。原理:[Web diff 卡片笔记](../../../.agents/notes/implemented/feature/2026-07-30-web-diff-card.zh.md)。
68
+ ### 几何与溢出
30
69
 
31
- ## 搜索结果
70
+ 输出卡片共享同一套几何模型:`white-space: pre` 并横向滚动,让按列对齐的内容保持对齐;超过 `maxLines`(默认 16)时折叠为头部切片加尾部切片,由展开按钮控制,长正文不会撑高卡片。`TerminalBlock` 把 ANSI 解析为 React span,并带逐行列缓冲处理光标移动,遵循行内擦除、制表位与字符宽度。
32
71
 
33
- `SearchBlock` 渲染一次已完成的搜索,并通过 `kind` 判别,由一个组件处理两种结果。`matches`(grep)将每个文件显示为粗体路径头及其 `lineNumber: line` 行,各文件组均可折叠;`paths`(glob)显示扁平的路径列表。两者都摊平成一个行列表,由高度上限对其做头尾切片(默认 16,与 `TerminalBlock` 相同的切分算法),且都不软换行:较长的匹配行或路径会横向滚动而非折行。当工具截断结果时,banner 摘要会包含截断前的总数(grep 为 `显示 X / 共 N 处匹配 · K 个文件`,glob 为 `显示 X / 共 N 个路径`),使卡片绝不把截断后的结果呈现为完整结果;无论是否触及上限或哪些组处于折叠状态,复制控件都会写入完整的结构化结果。几何结构与 `CodeBlock`/`TerminalBlock` 一致。原理:[Web 搜索卡片笔记](../../../.agents/notes/implemented/feature/2026-07-30-web-search-card.zh.md)。
72
+ </details>
34
73
 
35
- ## Web 检索
74
+ -----
36
75
 
37
- `WebBlock` 渲染一次已完成的 web 检索,用一个组件绘制 `web` 渲染意图的两种 kind(由 `kind` 判别)。`search` 在有序引用列表上方显示可选的提供方回答(通过 `MarkdownText`):每个 source 是一个安全外链,以其标题为标签,或以其主机名为标签,当 URL 无法解析或没有主机名(`file:`/`data:` URL)时回退到原始 URL,因此标签绝不为空;其下渲染 snippet 与发布日期。只有 http(s) URL 会成为锚点(设置 `target`/`rel`)——这是 `MarkdownText` 对不受信任链接所用 allowlist 的 http(s) 子集(该 allowlist 还允许 `mailto:`,此处排除);任何其他 URL 渲染为纯文本。整份列表渲染在一个定高滚动容器里(`max-height: 320px`、`overflow-y: auto`),因此超出该高度的列表在原地纵向滚动,而不是把卡片撑高;`<li value>` 固定每个 source 的引用编号,从 1 起连续,而不依赖 `<ol>` 的隐式计数。当一次 search 合法地返回无 answer 且无 source 时,卡片显示一个明确的空状态提示,而不是空的 `<ol>`(chat 行不呈现原始 result content)。`fetch` 显示一个紧凑摘要:带链接的最终 URL 及其 HTTP 状态。两者都会标记一次被截断的检索。原理:[Web result 卡片笔记](../../../.agents/notes/implemented/feature/2026-07-30-web-result-card-frontend.zh.md)与[来源滚动笔记](../../../.agents/notes/implemented/feature/2026-08-03-web-search-source-scroll.zh.md)。
76
+ <a id="further-exploration"></a>
77
+ ## 进一步探索
38
78
 
79
+ 以下页面说明这些原子组件在客户端技术栈与设计系统中的位置。
80
+
81
+ - [ui-renderer](../ui-renderer/README.zh.md)——挂载组装后应用并绑定 slot 数据的 React 渲染器。
82
+ - [ui-tool](../ui-tool/README.zh.md)——拼装这些输出卡片的工具调用展示层。
83
+ - [ui-conversation](../ui-conversation/README.zh.md)——渲染 markdown 回复与工具卡片的聊天界面。
84
+ - [ui-theme](../ui-theme/README.zh.md)——这些原子组件样式所依赖的 `--dsw-*` token 体系。
85
+ - [Web 样式](../../../docs/web-styling.zh.md)——Web 客户端组件的权威样式规则。
86
+
87
+ -----
88
+
89
+ <a id="model-experience"></a>
39
90
  ## 模型体验
40
91
 
41
- 无。该包在浏览器中渲染纯 React 原子组件;这里没有任何内容进入模型请求。
92
+ 无。该包是浏览器端 UI 插件层,不注册任何面向模型的内容。
42
93
 
43
94
  #### KV Cache 影响
44
95
 
45
96
  无;该包既不组装也不发送提供方请求。
46
97
 
47
- ## 已知限制与暂缓事项
98
+ ## 已知限制与延期工作
99
+
100
+ <a id="known-limitations-and-deferred-work"></a>
101
+
102
+
103
+ 这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。
104
+
105
+ - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。
106
+ - **字形级图标是重新绘制的近似版本**:鱼形标志与闪光标记来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
107
+ - **`Pill` 与 `Input` 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。
108
+ - **`StateDot` 没有 `Active` 变体**:支持的状态为 done、warning、ongoing 和 error。
109
+ - **面向用户的文案必须由渲染点提供**:这些原子组件是 zero-Cordis 的,拿不到 `ctx.locale`;各功能必须通过 primitive 的带类型 prop 提供完整本地化 label(见[决策](../../../.agents/notes/implemented/architecture/2026-08-23-locale-owned-client-ui-copy.zh.md))。
110
+ - **`TerminalBlock` 不是终端模拟器**:它渲染已结束或仍在运行的命令输出,而不是交互式会话:SGR 颜色、回车、退格、行内擦除、制表位与字符宽度会被遵循;绝对光标定位、清屏与备用屏幕序列会被剥离。
111
+
112
+ <a id="dev-note"></a>
113
+ ### 开发备注
114
+
115
+ <details>
116
+ <summary>维护者的工作上下文——点击展开</summary>
117
+
118
+ 无。
48
119
 
49
- - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。内联链接以及在同一次解析内完成解析的引用不受影响。
50
- - **字形级图标是重新绘制的近似版本**:鱼形标志(以及 ui-conversation 持有的闪光图标)来自字体字形,而本地设计数据无法导出其矢量几何;在获得精确导出路径前,使用手工重建版本代替。
51
- - **Pill 与 Input 没有设计来源**:两个原子组件均自行定义;与其相似的侧边栏搜索字段和视图标签条由消费方组合,不是这些原子组件。
52
- - **StateDot 没有 `Active` 变体**:支持的状态为 done、warning、ongoing 和 error。
53
- - **面向用户的文案经 label props 本地化,默认值为原中文字面量**:这些原子组件是 zero-cordis 的,拿不到 `ctx.locale`,因此 `HoverCard`(`copyLabel`/`copiedLabel`)、`TerminalBlock`(`labels`)、`JsonTree`(`labels`)、`CodeBlock`(`copyLabel`/`copiedLabel`)、`MarkdownText`(`codeLabels`)、`JsonBlock`(`truncatedLabel`)、`ConnectionBanner`(`label`)和 `Modal`(`closeLabel`)都把文案作为可选 props 接收。已本地化的插件用自己的 `t` 席位传入字典驱动的 label;什么都不传的消费方得到的就是这些默认值。`WebBlock` 尚未跟进这一模式:它的来源列表截断提示与 fetch 截断提示、以及空搜索提示仍是内联中文,待同样的 label-prop 处理。
54
- - **`TerminalBlock` 不是终端模拟器**:它渲染已结束或仍在运行的命令输出,而不是交互式会话:SGR 颜色与属性会被遵循,进度行所用的行内光标移动同样被遵循——回车、退格、行内擦除、制表位与字符宽度。绝对光标定位、清屏与备用屏幕序列会被剥离。基础 16 色中的洋红与青色没有对应 token,保持字面 rgb。
120
+ </details>
@@ -0,0 +1,107 @@
1
+ .indicator {
2
+ flex: none;
3
+ display: inline-grid;
4
+ grid-template-columns: 14px max-content;
5
+ align-items: center;
6
+ column-gap: 4px;
7
+ height: 32px;
8
+ padding: 0 10px;
9
+ box-sizing: border-box;
10
+ border: none;
11
+ border-radius: 8px;
12
+ font-family: inherit;
13
+ font-size: 12px;
14
+ font-weight: 500;
15
+ line-height: 18px;
16
+ white-space: nowrap;
17
+ transition: background-color 160ms ease-out, color 160ms ease-out;
18
+ }
19
+
20
+ .warning {
21
+ background: var(--dsw-alias-state-warn-tertiary);
22
+ color: var(--dsw-alias-state-warn-label);
23
+ cursor: pointer;
24
+ }
25
+
26
+ .warning:active {
27
+ background: color-mix(
28
+ in srgb,
29
+ var(--dsw-alias-state-warn-tertiary),
30
+ var(--dsw-alias-state-warn-primary) 10%
31
+ );
32
+ }
33
+
34
+ .warning:focus-visible {
35
+ outline: 2px solid var(--dsw-alias-state-warn-label);
36
+ outline-offset: 2px;
37
+ }
38
+
39
+ .success {
40
+ background: var(--dsw-alias-state-success-tertiary);
41
+ color: var(--dsw-alias-state-success-primary);
42
+ }
43
+
44
+ .icon {
45
+ display: grid;
46
+ place-items: center;
47
+ width: 14px;
48
+ height: 14px;
49
+ }
50
+
51
+ .label {
52
+ display: grid;
53
+ text-align: left;
54
+ }
55
+
56
+ .stateLabel,
57
+ .hoverLabel,
58
+ .sizeLabel {
59
+ grid-area: 1 / 1;
60
+ }
61
+
62
+ .sizeLabel {
63
+ visibility: hidden;
64
+ }
65
+
66
+ .warning:is(:hover, :focus-visible) .stateLabel {
67
+ visibility: hidden;
68
+ }
69
+
70
+ .hoverLabel {
71
+ visibility: hidden;
72
+ }
73
+
74
+ .warning:is(:hover, :focus-visible) .hoverLabel {
75
+ visibility: visible;
76
+ }
77
+
78
+ .dots {
79
+ display: inline-block;
80
+ width: 1.5em;
81
+ text-align: left;
82
+ }
83
+
84
+ .secondDot {
85
+ animation: reveal-second-dot 1.5s step-end infinite;
86
+ }
87
+
88
+ .thirdDot {
89
+ animation: reveal-third-dot 1.5s step-end infinite;
90
+ }
91
+
92
+ @keyframes reveal-second-dot {
93
+ 0%, 33.32% { visibility: hidden; }
94
+ 33.33%, 100% { visibility: visible; }
95
+ }
96
+
97
+ @keyframes reveal-third-dot {
98
+ 0%, 66.65% { visibility: hidden; }
99
+ 66.66%, 100% { visibility: visible; }
100
+ }
101
+
102
+ @media (prefers-reduced-motion: reduce) {
103
+ .secondDot,
104
+ .thirdDot {
105
+ animation: none;
106
+ }
107
+ }
@@ -1,9 +1,3 @@
1
- /* Geometry mirrors CodeBlock/TerminalBlock (12px radius, code-block surface +
2
- banner row, markdown code-block font) so a diff card reads as one family with
3
- a fenced block and a terminal card. The deliberate divergence, shared with
4
- TerminalBlock: the body keeps `white-space: pre` and scrolls horizontally,
5
- because folding a source line destroys the indentation a diff is read by. */
6
-
7
1
  .block {
8
2
  --dsl-diff-radius: 12px;
9
3
  --dsl-diff-line-height: 22px;
@@ -1,4 +1,10 @@
1
- /* Shared disclosure header: [16px leading] gap 6 [title 14/24]. */
1
+ /* Shared disclosure header: [16px leading] gap 6 [title 13/24] at the default
2
+ size. The Settings font-size preference moves the row through the
3
+ body-published axis: title size follows the secondary tier
4
+ (--dsh-content-font-size-secondary: one step under the body — setting −1 at
5
+ ≤14, setting −2 above), and the row height, leading box, and glyph edge
6
+ shift by the body px delta so the icon keeps its optical share of the
7
+ line. */
2
8
 
3
9
  .root {
4
10
  display: flex;
@@ -12,7 +18,7 @@
12
18
  overflow: hidden;
13
19
  display: flex;
14
20
  align-items: center;
15
- height: 24px;
21
+ height: calc(24px + var(--dsh-content-font-delta, 0px));
16
22
  min-width: 0;
17
23
  }
18
24
 
@@ -23,8 +29,8 @@
23
29
  .leading {
24
30
  position: relative;
25
31
  flex: none;
26
- width: 16px;
27
- height: 16px;
32
+ width: calc(16px + var(--dsh-content-font-delta, 0px));
33
+ height: calc(16px + var(--dsh-content-font-delta, 0px));
28
34
  display: inline-flex;
29
35
  align-items: center;
30
36
  justify-content: center;
@@ -35,6 +41,15 @@
35
41
  color: var(--dsw-alias-label-tertiary);
36
42
  }
37
43
 
44
+ /* Flow-row glyphs render at 14px inside the 16px box; the CSS edge overrides
45
+ each svg's own width/height attributes so every registered icon scales
46
+ without a per-callsite size prop. StateDot (its svg carries data-state)
47
+ stays at its fixed figma size — it is a status mark, not text furniture. */
48
+ .leading svg:not([data-state]) {
49
+ width: calc(14px + var(--dsh-content-font-delta, 0px));
50
+ height: calc(14px + var(--dsh-content-font-delta, 0px));
51
+ }
52
+
38
53
  button.leading {
39
54
  cursor: pointer;
40
55
  }
@@ -63,7 +78,7 @@ button.leading {
63
78
 
64
79
  .title {
65
80
  flex: none;
66
- font-size: 14px;
67
- line-height: 24px;
81
+ font-size: var(--dsh-content-font-size-secondary, 13px);
82
+ line-height: calc(24px + var(--dsh-content-font-delta, 0px));
68
83
  color: var(--dsw-alias-label-secondary);
69
84
  }
@@ -1,9 +1,3 @@
1
- /* Geometry mirrors CodeBlock (12px radius, code-block surface + banner row,
2
- markdown code-block font) so a read card and a fenced code block read as one
3
- family. Content keeps `white-space: pre` and scrolls horizontally rather than
4
- folding, because a source line's indentation is part of what a reader is
5
- reading. */
6
-
7
1
  .block {
8
2
  --dsl-read-radius: 12px;
9
3
  --dsl-read-line-height: 22px;
@@ -1,9 +1,3 @@
1
- /* Geometry mirrors CodeBlock and TerminalBlock (12px radius, code-block
2
- surface + banner row, markdown code-block font) so a search card reads as one
3
- family with them. The deliberate divergence they share: the result rows keep
4
- `white-space: pre` and scroll horizontally, because folding a long match line
5
- or path destroys the alignment a reader scans by. */
6
-
7
1
  .block {
8
2
  --dsl-search-radius: 12px;
9
3
  --dsl-search-line-height: 22px;
@@ -15,8 +9,6 @@
15
9
  border-radius: var(--dsl-search-radius);
16
10
  }
17
11
 
18
- /* The banner: result summary on the left, the copy control holding its
19
- intrinsic width on the right. */
20
12
  .header {
21
13
  display: flex;
22
14
  align-items: center;
@@ -1,9 +1,3 @@
1
- /* Geometry mirrors CodeBlock (12px radius, code-block surface + banner rows,
2
- markdown code-block font) so a terminal card and a fenced code block read as
3
- one family. The one deliberate divergence: output keeps `white-space: pre`
4
- and scrolls horizontally, because folding a column-aligned command's output
5
- destroys its alignment. */
6
-
7
1
  .block {
8
2
  --dsl-terminal-radius: 12px;
9
3
  --dsl-terminal-line-height: 22px;
@@ -1,8 +1,7 @@
1
- /* Transient top-center banner (DeepSeek Chat toast look): contrast fill,
2
- inverted label, slide-in, then hold-and-fade. The fade delay/duration MUST
3
- agree with HOLD_MS/FADE_MS in Toast.tsx: the component unmounts at their
4
- sum, so a mismatched sheet either cuts the fade or leaves an invisible
5
- banner blocking nothing. */
1
+ /* The fade delay comes from the component as `--dsh-toast-hold`, so one value
2
+ drives both the unmount timer and this animation; the fallback matches the
3
+ component's own default. The fade DURATION still has to agree with FADE_MS
4
+ in Toast.tsx, which no owner varies. */
6
5
 
7
6
  .toast {
8
7
  position: fixed;
@@ -11,9 +10,7 @@
11
10
  /* Above the 1000 the image lightbox backdrop uses: a failure reported while
12
11
  a preview is open must stay readable. */
13
12
  z-index: 1100;
14
- /* Purely an announcement: it must never intercept clicks — in particular
15
- after the CSS fade finished while a throttled background-tab timer has
16
- not yet unmounted the still-hit-testable fixed element. */
13
+ /* Announcements never intercept clicks. */
17
14
  pointer-events: none;
18
15
  display: flex;
19
16
  align-items: center;
@@ -29,7 +26,7 @@
29
26
  transform: translateX(-50%);
30
27
  animation:
31
28
  dsh-toast-in 160ms ease-out,
32
- dsh-toast-fade 1000ms ease 3000ms forwards;
29
+ dsh-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;
33
30
  }
34
31
 
35
32
  .icon {
@@ -65,6 +62,6 @@
65
62
  not movement) still ends the banner before the timed unmount. */
66
63
  @media (prefers-reduced-motion: reduce) {
67
64
  .toast {
68
- animation: dsh-toast-fade 1000ms ease 3000ms forwards;
65
+ animation: dsh-toast-fade 1000ms ease var(--dsh-toast-hold, 3000ms) forwards;
69
66
  }
70
67
  }
@@ -1,10 +1,3 @@
1
- /* Visual spec mirrors deepsuite @deepseek/ui Tooltip.css (size m, no arrow),
2
- except padding tightened 6/12 -> 3/7, type 14/22 -> 13/20, radius
3
- 10 -> 8, a tooltip-bg plate,
4
- one text color across both themes (the plate stays dark in light and dark
5
- mode). Behavior (fixed positioning off the anchor rect) is local — the
6
- upstream Floating stack is intentionally not vendored. */
7
-
8
1
  .bubble {
9
2
  position: fixed;
10
3
  z-index: 100;